처음으로 Java를 배우기 시작했을 때, 여러 스레딩 상황이 발생했을 때 Synchronized가 발생했습니다. 당시 우리와 비교할 때 동기화 된 것은 매우 마술적이고 강력했습니다. 당시 우리는 "동기화"라는 이름을 주었고, 이는 멀티 스레딩 상황을 해결하기에 좋은 약이되었습니다. 그러나 우리가 배우면서, 우리는 동기화 된 것이 헤비급 잠금 장치이며 잠금 장치에 비해 너무 부피가 커져서 그렇게 효율적이지 않고 천천히 포기한다는 것을 알고 있습니다.
틀림없이, 동기화 된 Javas SE 1.6의 다양한 최적화로 동기화 된 동기화는 그렇게 무겁게 보이지 않습니다. 아래에서 LZ를 따라 동기화 된 구현 메커니즘, Java를 최적화하는 방법, 최적화 메커니즘 잠금, 저장 구조 및 업그레이드 프로세스를 탐색합니다.
구현 원리
동기화 된 것은 메소드 또는 코드 블록이 실행되도록 할 수 있으며, 하나의 메소드만이 임계 영역에 동시에 들어갈 수 있습니다. 동시에 공유 변수의 메모리 가시성을 보장 할 수도 있습니다.
Java의 모든 객체는 동기화를 구현하기위한 동기화의 기초 인 잠금으로 사용할 수 있습니다.
공통 동기화 방법, 잠금은 현재 인스턴스 객체의 정적 동기화 방법입니다. 잠금은 현재 클래스 객체 동기화 메소드 블록입니다. 잠금은 괄호 안의 객체입니다. 스레드가 동기화 코드 블록에 액세스 할 때 먼저 동기화 코드를 실행하려면 잠금을 가져와야합니다. 예외가 종료되거나 던져지면 자물쇠를 해제해야합니다. 그렇다면이 메커니즘을 어떻게 구현합니까? 먼저 간단한 코드를 살펴 보겠습니다.
public class synchronizedtest {public synchronized void test1 () {} public void test2 () {synchronized (this) {}}}Javap 도구를 사용하여 생성 된 클래스 파일 정보를보고 동기화 구현을 분석하십시오.
위에서 볼 수 있듯이 동기화 코드 블록은 Monitorenter 및 MonitoreXit 명령을 사용하여 구현됩니다. 동기화 메소드 (기본 JVM 구현을 볼 필요가 있음을 보여주지 않음)는 메소드 수정 자의 ACC_SynChronized 구현에 의존합니다.
동기 코드 블록 : Moniterenter 명령어가 동기화 코드 블록의 시작 위치에 삽입되고 MonitoreXit 명령이 동기화 코드 블록의 끝 위치에 삽입됩니다. JVM은 각 모니터 센터가 이에 해당하는 모니토어가 있도록해야합니다. 모든 객체에는 모니터와 관련된 모니터가 있으며 모니터가 유지되면 잠겨 있습니다. 스레드가 Moniterenter 명령을 실행하면 객체에 해당하는 모니터 소유권을 얻으려고합니다. 즉, 객체의 잠금을 얻으려고합니다.
동기화 된 방법 : 동기화 된 메소드는 일반 메소드 호출로 변환되고 다음과 같은 반환 지침 : InvokeVirtual 및 Areturn 지침. 동기화 된 수정 된 메소드를 구현하기위한 VM 바이트 코드 레벨에는 특별 명령이 없습니다. 대신, 메소드 access_flags 메소드의 동기화 된 플래그 위치 1은 클래스 파일의 메소드 테이블에 배치되며, 메소드가 동기화 된 메소드이고 메소드를 호출하는 객체를 사용하여 klass를 잠금 객체로 표시하는 메소드를 사용하는 객체를 사용합니다 (http://www.vevb.com/129245.htm).
계속 분석하겠습니다. 그러나 더 깊이 들어가기 전에 Java 객체 헤더와 모니터라는 두 가지 중요한 개념을 이해해야합니다.
Java 객체 헤더, 모니터
Java 객체 헤더 및 모니터는 동기화 된 구현의 기초입니다! 다음은이 두 개념에 대한 자세한 소개입니다.
자바 객체 헤더
동기화에 사용되는 잠금 장치는 Java 개체 헤더에 있습니다. Java 객체 헤더는 무엇입니까? 핫스팟 가상 머신의 객체 헤더에는 주로 데이터의 두 부분이 포함됩니다 : mark word (mark field)와 klass pointer (유형 포인터). Klass Point는 객체의 클래스 메타 데이터에 대한 포인터입니다. 가상 머신은이 포인터를 사용하여 객체의 클래스 인스턴스를 결정합니다. 마크 워드는 객체의 자체 런타임 데이터를 저장하는 데 사용됩니다. 경량 잠금 장치 및 바이어스 잠금 장치를 구현하는 데 열쇠가 있으므로 다음에 중점을 둡니다.
마크 단어.
Mark Word는 해시 코드 (해시 코드), GC 생성 연령, 잠금 상태 플래그, 스레드, 바이어스 스레드 ID, 바이어스 타임 스탬프 등과 같은 객체 자체의 런타임 데이터를 저장하는 데 사용됩니다. Java 객체 헤더는 일반적으로 2 개의 기계 코드를 차지합니다 (32 비트 가상 기계에서 1 기계 코드는 4 바인드 코드입니다. JVM 가상 머신은 Java 객체의 메타 데이터 정보를 통해 Java 객체의 크기를 결정할 수 있지만 배열의 메타 데이터에서 배열의 크기를 확인할 수 없으므로 조각은 배열의 길이를 기록하는 데 사용됩니다. 다음 그림은 Java 객체 헤더 (32 비트 가상 머신)의 스토리지 구조입니다.
객체 헤더 정보는 객체 자체에 의해 정의 된 데이터와 무관 한 추가 스토리지 비용입니다. 그러나 가상 머신의 공간 효율성을 고려할 때 Mark Word는 매우 작은 공간 메모리에 가능한 한 많은 데이터를 저장하기 위해 고정되지 않은 데이터 구조로 설계되었습니다. 물체의 상태에 따라 자체 저장 공간을 재사용합니다. 즉, Mark Word는 프로그램의 작동에 따라 변경 될 것이며 변경 상태는 다음과 같습니다 (32 비트 가상 머신).
Java 객체 헤더에 대한 간단한 소개, 모니터를 살펴 보겠습니다.
감시 장치
모니터 란 무엇입니까? 우리는 그것을 동기화 도구 또는 동기화 메커니즘으로 이해할 수 있으며, 이는 일반적으로 객체로 묘사됩니다.
모든 것이 객체 인 것처럼 모든 Java 객체는 자연스러운 모니터이며 각 Java 객체는 모니터가 될 가능성이 있습니다. Java 디자인에서는 각 Java 객체가 자궁에서 나오기 때문에 보이지 않는 잠금 장치를 가지고 있기 때문입니다. 이를 내부 잠금 장치 또는 모니터 잠금이라고합니다.
모니터는 스레드가 개인적으로 소유 한 데이터 구조입니다. 각 스레드에는 사용 가능한 모니터 레코드 목록과 전역 사용 가능한 목록이 있습니다. 잠긴 각 객체는 모니터 (객체 헤더의 마석 단어의 잠금 단어가 모니터의 시작 주소를 가리 킵니다)와 연결됩니다. 동시에, 모니터에는 잠금 장치를 소유 한 스레드의 고유 식별자를 저장하는 소유자 필드가 있으며, 이는 잠금이 스레드에 의해 점유되었음을 나타냅니다. 구조는 다음과 같습니다.
소유자 : 처음에 NULL은 현재 MonitorRecord를 소유하고 있음을 의미합니다. 스레드가 잠금을 성공적으로 소유하면 스레드의 고유 한 신원을 저장하고 잠금이 해제되면 NULL로 설정됩니다.
EntryQ : System Mutx (Semaphore)를 연결하여 MonitorRecord를 잠그려고 시도하는 모든 스레드를 차단합니다.
RCTHIS : MonitorRecord에서 차단되거나 대기중인 모든 스레드의 수를 나타냅니다.
둥지 : 재입국 잠금 장치 계산에 사용됩니다.
해시 코드 : 객체 헤더에서 복사 된 해시 코드 값을 저장합니다 (GCAGE를 포함 할 수도 있음).
후보 : 불필요한 차단을 피하거나 실이 깨어날 때까지 기다리는 데 사용됩니다. 하나의 스레드만이 매번 잠금을 성공적으로 소유 할 수 있기 때문입니다. 잠금을 방출하는 이전 스레드가 매번 모든 차단 또는 대기 스레드를 깨우면 불필요한 컨텍스트 전환 (차단에서 준비로, 경쟁 잠금 고장으로 인해 다시 차단)이 발생하여 심각한 성능 저하로 이어집니다. 후보자는 두 가지 가능한 값 만 있습니다. 0은 자물쇠를 놓고 경쟁하기 위해 후속 스레드를 깨우기 위해 최대 1 개의 수단을 깨울 필요가 없다는 것을 의미합니다.
참조 : Java 동시성으로 동기화 된 것에 대해 이야기하십시오
우리는 동기화 된 것이 헤비급 잠금 장치이며 효율성이 좋지 않다는 것을 알고 있습니다. 동시에,이 개념은 항상 우리의 마음에있었습니다. 그러나 동기화의 구현은 JDK1.6에서 최적화되어 그다지 무겁지 않습니다. 그렇다면 JVM은 어떤 최적화 방법을 사용합니까?
잠금 최적화
JDK1.6은 스핀 잠금 장치, 적응 형 스핀 잠금 장치, 잠금 제거, 잠금 장치, 바이어스 잠금, 경량 잠금 및 기타 기술과 같은 잠금 장치 구현에 대한 많은 최적화를 도입합니다.
잠금 상태, 바이어스 잠금 상태, 경량 잠금 상태 및 헤비급 잠금 상태의 4 가지 주요 잠금 상태가 있습니다. 그들은 치열한 경쟁으로 점차 업그레이드 할 것입니다. 잠금 장치를 업그레이드 할 수 있으며 다운 그레이드 할 수 없습니다. 이 전략은 자물쇠를 획득하고 방출하는 효율성을 향상시키는 것입니다.
스핀 잠금
스레드 차단 및 모닝은 CPU가 사용자 상태에서 핵심 상태로 변경해야합니다. 자주 차단 및 모닝은 CPU의 큰 작업이며, 시스템의 동시 성능에 필연적으로 많은 압력을 가할 것입니다. 동시에, 우리는 많은 응용 분야에서 객체 잠금의 잠금 상태가 매우 짧은 시간 동안 만 지속될 것임을 발견했습니다. 이 짧은 시간 동안 실을 자주 차단하고 깨우는 것은 매우 가치가 없습니다. 스핀 잠금 장치가 도입됩니다.
스핀 잠금 장치는 무엇입니까?
소위 스핀 잠금 장치는 스레드가 일정 기간 동안 대기하고 잠금을 고정하는 스레드가 잠금을 빠르게 해제하는지 확인하기 위해 즉시 매달리지 않도록하는 것입니다. 기다리는 방법? 무의미한주기 (스핀) 만 수행하십시오.
스핀 대기는 차단을 대체 할 수 없습니다. 프로세서 수에 대한 요구 사항에 대해 이야기합시다 (멀티 코어, 현재 단일 코어 프로세서가없는 것 같습니다). 스레드 전환으로 인한 오버 헤드를 피할 수 있지만 프로세서 시간이 걸립니다. 잠금을 고정하는 스레드가 잠금을 빠르게 방출하면 스핀 효율이 매우 좋습니다. 반대로, 스핀 스레드는 처리 리소스를 헛되게 소비합니다. 의미있는 일을하지 않을 것입니다. 그것은 일반적으로 구덩이를 차지하고 똥을하지 않으므로 대신 성능 폐기물로 이어질 것입니다. 따라서 스핀 대기 시간 (스핀 수)에는 한계가 있어야합니다. 스핀이 정의 된 시간을 초과하고 여전히 잠금을 얻지 못하면 매달려 야합니다.
Spinlock은 JDK1.4.2에 도입되어 기본적으로 꺼져 있지만 -xx :+Usespinning으로 켜질 수 있으며 JDK1.6에서 기본적으로 켜집니다. 동시에 스핀의 기본 수는 10 배이며, 파라미터 -xx : preblockspin으로 조정할 수 있습니다.
파라미터 -xx : preblockspin을 통해 스핀 잠금의 스핀 스핀 수를 조정하면 많은 불편 함이 발생합니다. 매개 변수를 10으로 조정하지만 시스템의 많은 스레드가 나가면 잠금을 해제하면 (한두 번 회전하면 잠금을 얻을 수 있음) 당황 하시겠습니까? 따라서 JDK1.6은 적응 형 스핀 잠금 장치를 도입하여 가상 머신을 더 똑똑하고 똑똑하게 만듭니다.
스핀 잠금 장치에 적응하십시오
JDK1.6은 더 똑똑한 스핀 잠금 장치, 즉 적응 형 스핀 잠금 장치를 소개합니다. 소위 적응은 스핀 수가 더 이상 고정되지 않았 음을 의미하며, 동일한 잠금 장치의 스핀 타임과 잠금 소유자의 상태에 의해 결정됩니다. 어떻게해야합니까? 스레드가 성공적으로 회전하면, 가상 기계는 마지막으로 성공했을 때 이후 스핀이 다시 성공할 가능성이 높으며 스핀이 더 많은 시간을 기다릴 수 있다고 믿기 때문에 다음 번에 스핀 수가 더 높아질 것입니다. 반대로, 특정 잠금 장치에 대해 스핀이 성공할 수있는 경우, 프로세서 리소스를 폐기하지 않기 위해 향후 잠금 장치가 필요할 때 스핀 수가 줄어들거나 생략됩니다.
적응 형 스핀 잠금 장치를 사용하면 프로그램 작동 및 성능 모니터링 정보가 계속 향상됨에 따라 가상 머신의 프로그램 잠금 상태 예측이 점점 더 정확 해지고 가상 머신이 더 똑똑해집니다.
잠금 제거
데이터의 무결성을 보장하기 위해서는 작업을 수행 할 때 작업 의이 부분을 동기화해야하지만 경우에 따라 JVM은 공유 데이터 경쟁의 가능성이 없음을 감지하므로 JVM이 이러한 동기화 잠금을 잠그는 이유입니다. 잠금 제거의 기초는 탈출 분석을위한 데이터 지원입니다.
경쟁이없는 경우 왜 여전히 자물쇠를 추가해야합니까? 따라서 잠금 제거는 무의미하게 잠금을 요청하는 시간을 절약 할 수 있습니다. 가상 머신이 데이터 흐름 분석이 사용되는지 여부를 결정하는 데 가변 탈출이 필요한지 여부는 여전히 미국 프로그래머에게 불분명합니까? 데이터 경쟁이 없다는 것을 분명히 알고있는 코드 블록 앞에 동기화를 추가 할 것인가? 그러나 때때로 프로그램은 우리가 생각하는 것이 아닙니다. 잠금 장치를 표시하지는 않지만 Stringbuffer, Vector, Hashtable 등과 같은 JDK 내장 API를 사용할 때는 현재 보이지 않는 잠금 작업이 있습니다. 예를 들어 StringBuffer의 append () 메소드 및 벡터의 add () 메소드 :
public void vectortest () {vector <string> vector = new vector <string> (); for (int i = 0; i <10; i ++) {vector.add (i+""); } system.out.println (벡터); }이 코드를 실행할 때 JVM은 변수 벡터가 vectortest () 메소드에서 벗어나지 않음을 명확하게 감지 할 수 있으므로 JVM은 벡터 내부의 잠금 작동을 대담하게 제거 할 수 있습니다.
거칠게
동기화 잠금을 사용할 때 동기화 블록의 범위는 가능한 한 작아야합니다. 공유 데이터의 실제 범위에서만 동기화됩니다. 이것의 목적은 가능한 한 많이 동기화 해야하는 작업의 수를 최소화하는 것입니다. 잠금 경쟁이 있으면 잠금을 기다리는 스레드도 가능한 빨리 잠금을 얻을 수 있습니다.
대부분의 경우 위의 견해는 정확하며 LZ는 항상이 견해를 준수했습니다. 그러나 일련의 연속 잠금 및 잠금 해제 작업이 불필요한 성능 손실로 이어질 수 있으므로 잠금의 개념이 소개됩니다.
잠금 중상의 개념은 이해하기 쉽습니다. 이는 여러 연속 잠금 및 잠금 해제 작업을 함께 연결하고 더 큰 범위의 잠금으로 확장하는 것입니다. 위의 예에서 볼 수 있듯이 벡터는 추가 할 때마다 잠금 작업을 추가해야합니다. JVM은 동일한 객체 (벡터)가 지속적으로 잠금 및 잠금 해제되었음을 감지하고, 잠금 및 잠금 해제 된 작업이 더 넓은 범위의 잠금 및 잠금 해제 작업이 Loop 외부로 이동됩니다.
경량 잠금
경량 잠금 장치를 도입하는 주요 목적은 여러 스레드가없는 여러 경쟁의 전제에 따라 운영 체제 뮤텍스를 사용하여 발생하는 성능 소비를 줄이는 것입니다. 바이어스 잠금 기능이 꺼지거나 여러 스레드가 바이어스 잠금을 위해 경쟁하면 바이어스 잠금이 가벼운 잠금으로 업그레이드되고 경량 잠금이 시도되고 단계는 다음과 같습니다.
자물쇠를 얻으십시오
현재 객체가 잠금 상태인지 여부를 결정하십시오 (Hashcode, 0, 01). 그렇다면 JVM은 먼저 현재 스레드의 스택 프레임에 잠금 레코드라는 공간을 생성하여 잠금 객체의 현재 마크 워드 사본을 저장합니다 (공무원은이 사본에 변위 접두사를 추가합니다. 그렇지 않으면 단계 (3)을 실행하십시오.
JVM은 CAS 작업을 사용하여 레코드를 잠그는 것을 가리키는 수정으로 객체의 마크 단어를 업데이트하려고합니다. 잠금 장치가 컨테스트되었음을 성공적으로 나타내면 잠금 플래그는 00으로 변경되고 (객체가 경량 잠금 상태에 있음을 나타내고) 동기화 작업을 수행합니다. 실패하면 단계 (3)을 수행하십시오.
현재 객체의 마크 단어가 현재 스레드의 스택 프레임을 가리키는 지 여부를 결정하십시오. 그렇다면 현재 스레드가 이미 현재 객체의 잠금을 고정 한 다음 동기 코드 블록을 직접 실행했음을 의미합니다. 그렇지 않으면 잠금 객체가 다른 스레드에 의해 압수되었음을 의미 할 수 있습니다. 이 시점에서 경량 잠금 장치는 헤비급 잠금으로 확장되어야하고 잠금 플래그 비트는 10이되고 나중에 대기 대기 스레드가 차단 상태로 들어갑니다.
잠금을 해제하십시오
경량 잠금 장치의 해제는 CAS 운영을 통해 수행되며 주요 단계는 다음과 같습니다.
획득 경량 잠금에서 변위 된 마크 워드에 저장된 데이터를 제거하십시오.
검색된 데이터를 Mark Word의 CAS 작업으로 바꾸십시오. 성공하면 잠금이 성공적으로 릴리스되었음을 의미합니다. 그렇지 않으면 실행됩니다 (3).
CAS 조작 교체가 실패하면 다른 스레드가 잠금을 얻으려고 시도하고 잠금을 방출하는 동안 매달린 스레드를 깨우야합니다.
경량 잠금의 경우 성능 향상의 기초는 "대부분의 자물쇠의 경우 전체 수명주기에 걸쳐 경쟁이 없을 것"이라는 것입니다. 이 기준이 상호 배타적 인 오버 헤드 외에도 추가 된 CAS 작업이 있습니다. 따라서 멀티 스레드 경쟁의 경우 경량 잠금 장치는 헤비급 잠금보다 느립니다.
다음 그림은 경량 잠금의 획득 및 릴리스 프로세스를 보여줍니다.
긍정적 인 자물쇠
바이어스 잠금 장치를 도입하는 주요 목적은 멀티 스레드 경쟁없이 불필요한 경량 잠금 실행 경로를 최소화하는 것입니다. 위에서 언급 한 것은 경량 잠금의 잠금 및 잠금 해제 작업이 여러 CAS 원자 지침에 의존해야한다고 언급했습니다. 그렇다면 바이어스 잠금은 어떻게 불필요한 CAS 운영을 줄입니까? 우리는 마크 작업의 구조를 볼 수 있습니다. 바이어스 잠금 장치인지 확인하면됩니다. 잠금 장치는 다음과 함께 식별됩니다. 처리 흐름은 다음과 같습니다.
자물쇠를 얻으십시오
마크 단어가 편견 가능한 상태인지, 즉 바이어스 잠금 1이고 잠금 식별 비트가 01인지 여부를 감지합니다.
편견이있는 상태 인 경우 스레드 ID가 현재 스레드 ID인지 테스트하십시오. 그렇다면 단계 (5)를 실행하고 그렇지 않으면 단계 (3)을 실행합니다.
스레드 ID가 현재 스레드 ID가 아닌 경우 CAS 작동에 의해 잠금이 경쟁합니다. 경쟁이 성공하면 Mark Word의 스레드 ID가 현재 스레드 ID로 대체됩니다. 그렇지 않으면 스레드 (4);
CAS 경쟁 잠금 장치는 실패하여 현재 다중 스레드 경쟁 상황이 있음을 증명합니다. 글로벌 보안 지점에 도달하면 바이어스 된 잠금을 얻는 스레드가 매달려지고 바이어스 잠금 장치가 경량 잠금으로 업그레이드 된 다음 보안 지점에서 차단 된 스레드가 계속 동기 코드 블록을 실행합니다.
동기 코드 블록을 실행하십시오
릴리스 잠금. 바이어스 된 잠금 장치의 출시는 경쟁만이 잠금을 해제 할 수있는 메커니즘을 채택합니다. 스레드는 바이어스 된 잠금 장치를 적극적으로 해제하지 않으며 다른 스레드가 경쟁 할 때까지 기다려야합니다. 바이어스 된 잠금 장치의 실행 취소는 글로벌 보안 지점을 기다려야합니다 (이 시점에서는 코드가 실행되지 않는다는 것입니다). 단계는 다음과 같습니다.
바이어스 잠금 장치로 스레드를 일시 중지하고 잠금 물체 석재가 여전히 잠긴 상태에 있는지 확인하십시오.
SU에 대한 바이어스를 잠금 해제하고 잠금 상태 (01) 또는 경량 잠금 상태로 복원하십시오.
다음 그림은 바이어스 잠금의 획득 및 릴리스 프로세스를 보여줍니다.
헤비급 잠금
체중 잠금 장치는 JVM에서 객체 모니터 (모니터)라고도합니다. 그것은 C의 MUTEX와 매우 유사합니다. MUTEX (0 | 1) 상호 배타적 기능을 갖는 것 외에도 세마포어의 기능을 구현하는 책임이 있습니다. 전자는 상호 배제를 담당하고 후자는 스레드 동기화에 사용됩니다.
헤비급 잠금 장치는 모니터 (모니터) 내부 물체를 통해 구현되며, 모니터의 본질은 기본 운영 체제의 뮤트 잠금 구현에 의존하는 것입니다. 운영 체제의 스레드 사이를 전환하려면 사용자 상태에서 커널 상태로 전환해야하며 스위칭 비용은 매우 높습니다.
요약
위의 내용은 Java에서 동기화 된 구현 원리에 대한 자세한 설명에 대한이 기사의 모든 내용입니다. 모든 사람에게 도움이되기를 바랍니다. 단점이 있으면 메시지를 남겨 두십시오. 이 사이트를 지원해 주신 친구들에게 감사드립니다!