1. 동기화 원리
JVM 사양에서는 JVM이 Monitor 객체의 진입 및 종료를 기반으로 메소드 동기화와 코드 블록 동기화를 구현한다고 규정하고 있지만 둘의 구현 세부 사항은 다릅니다. 코드 블록 동기화는 monitorenter 및 monitorexit 명령어를 사용하여 구현되며, 메소드 동기화는 다른 메소드를 사용하여 구현됩니다. 자세한 내용은 JVM 사양에 명시되어 있지 않지만 이 두 명령어를 사용하여 메소드 동기화를 구현할 수도 있습니다. monitorenter 명령어는 컴파일 후 동기화된 코드 블록의 시작 부분에 삽입되는 반면, monitorexit는 메서드 및 예외의 끝에 삽입됩니다. JVM은 각 monitorenter가 해당 monitorexit와 쌍을 이루어야 하는지 확인해야 합니다. 모든 개체에는 연결된 모니터가 있습니다. 모니터를 잡고 있으면 잠긴 상태가 됩니다. 스레드가 monitorenter 명령을 실행할 때 개체에 해당하는 모니터의 소유권을 얻으려고 시도합니다. 즉 개체의 잠금을 얻으려고 시도합니다.
2. Java 객체 헤더
는 Java 객체 헤더에 잠겨 있습니다. 개체가 배열 유형인 경우 가상 머신은 3워드(워드 너비)를 사용하여 개체 헤더를 저장합니다. 개체가 배열 유형이 아닌 경우 가상 머신은 2워드(워드 너비)를 사용하여 개체 헤더를 저장합니다. 32비트 가상 머신에서 1워드 너비는 4바이트, 즉 32비트와 같습니다.
Java 객체 헤더의 Mark Word에는 기본적으로 객체의 HashCode, 세대 연령 및 잠금 표시 비트가 저장됩니다. 32비트 JVM용 Mark Word의 기본 저장 구조는 다음과 같습니다.
작업 중에 Mark Word에 저장된 데이터는 잠금 플래그가 변경됨에 따라 변경됩니다. Mark Word는 다음 네 가지 유형의 데이터를 저장하도록 변경될 수 있습니다.
3. 여러 유형의 잠금
스레드를 차단하고 깨우려면 CPU가 필요합니다. 사용자 모드에서 전환하려면 핵심 상태로서 잦은 차단과 깨우기가 CPU에 큰 부담을 줍니다.
자바 SE 1.6에서는 잠금 획득과 해제로 인한 성능 소모를 줄이기 위해 '바이어스 잠금'과 '경량 잠금'을 도입했기 때문에 자바 SE 1.6에는 총 4가지 유형의 잠금이 있다. 상태, 잠금 없음 상태, 바이어스 잠금 상태, 경량 잠금 상태 및 중량 잠금 상태. 이는 경쟁 상황에 따라 점차 확대됩니다.
잠금 장치는 업그레이드할 수 있지만 다운그레이드할 수는 없습니다. 즉, 편향 잠금 장치를 경량 잠금 장치로 업그레이드한 후에 편향 잠금 장치로 다운그레이드할 수는 없습니다. 다운그레이드가 아닌 잠금 업그레이드 전략의 목적은 잠금 획득 및 해제의 효율성을 높이는 것입니다.
3.1 편향된 잠금
Hotspot의 저자는 이전 연구를 통해 대부분의 경우 잠금에 대한 멀티 스레드 경쟁이 없을 뿐만 아니라 항상 여러 번 잠금을 획득한다는 사실을 발견했습니다. 같은 스레드. 편향된 잠금의 목적은 스레드를 보호하는 것처럼 보이는 잠금을 획득한 후 스레드에 대한 CAS(잠금 재진입)의 오버헤드를 제거하는 것입니다.
편향 잠금에 대한 추가 이해
편향 잠금을 해제하기 위해 아무 것도 할 필요가 없습니다. 이는 MarkValue가 편향 잠금에 추가되었음을 의미합니다. 잠금 상태는 항상 유지되므로 동일한 스레드가 계속해서 잠그고 잠금 해제되더라도 오버헤드가 없습니다.
반면에 편향된 잠금은 경량 잠금보다 종료될 가능성이 더 높습니다. 경량 잠금은 잠금 경쟁이 발생할 때 무거운 잠금으로 업그레이드되는 반면, 일반 편향 잠금은 다른 스레드가 잠금에 적용될 때 경량 잠금으로 업그레이드됩니다. 이는 개체가 스레드 1에 의해 먼저 잠기고 잠금 해제된 다음 스레드 2에 의해 잠기고 잠금 해제되면 프로세스에 잠금 충돌이 없으며 편향된 잠금 실패도 발생한다는 차이점이 있습니다. 이번에는 먼저 발생해야 합니다. 그림에 표시된 대로 잠금 상태와 경량 잠금을 함께 사용합니다.
또한 JVM은 다중 스레드가 잠기는 상황에도 이 작업을 수행합니다. 잠금 경쟁이 없습니다. 최적화하는 것이 어색하게 들리지만, 상호 배제 외에도 두 개의 동기화된 스레드(앞에 하나, 뒤에 하나) 간에 동기화 관계가 발생할 수 있기 때문에 이러한 상황은 실제 응용 프로그램에서 실제로 발생할 수 있습니다. 공유 객체와의 관계 잠금 경합은 충돌이 없을 가능성이 높습니다. 이 경우 JVM은 잠금 편향 타임스탬프를 표현하기 위해 epoch를 사용합니다(실제로 타임스탬프를 생성하는 것은 상당히 비용이 많이 들기 때문에 epoch의 경우 공식적인 설명입니다. A). 대량 리바이어싱이라고 하는 유사한 메커니즘은 클래스의 객체가 서로 다른 스레드에 의해 잠기고 잠금 해제되는 상황을 최적화하지만 편향된 잠금을 비활성화하지 않고 클래스의 모든 인스턴스의 편향을 무효화합니다. 편향의 유효성을 나타내는 타임스탬프 이 값은 객체 할당 시 헤더 단어에 복사됩니다. 그러면 다음 번에 이 클래스의 인스턴스가 진행될 때 대량 리바이어스가 효율적으로 구현될 수 있습니다. 잠기면 코드는 헤더 단어에서 다른 값을 감지하고 개체를 현재 스레드로 리바이어스합니다.
바이어스 잠금 획득
스레드가 동기화된 블록에 액세스하여 잠금을 획득할 때, 잠금 편향 스레드 ID는 객체 헤더에 저장되고 스택 프레임의 잠금 기록은 나중에 동기화된 블록에 들어가고 나갈 때 잠금 및 잠금 해제를 위해 CAS 작업을 소비할 필요가 없습니다. 객체 헤더의 Mark Word가 현재 스레드를 가리키는 바이어스 잠금을 저장하는지 여부만 테스트하면 됩니다. 테스트가 성공하면 스레드가 잠금을 획득했다는 의미입니다. Mark Word의 바이어스 잠금 플래그는 1입니다(현재 바이어스된 잠금임을 나타냄). 설정되지 않은 경우 CAS가 잠금을 위해 경쟁하는 데 사용됩니다. CAS는 개체 헤더의 편향된 잠금을 현재 스레드로 가리킵니다.
편향된 잠금 해제
편향된 잠금은 잠금을 해제하기 전에 경쟁이 발생할 때까지 기다리는 메커니즘을 사용합니다. 따라서 다른 스레드가 편향된 잠금을 놓고 경쟁하려고 하면 해당 스레드가 편향된 잠금을 보유합니다. 잠금을 해제합니다. 편향된 잠금을 취소하려면 전역 안전 지점을 기다려야 합니다(이 시점에서는 바이트코드가 실행되지 않음). 먼저 편향된 잠금을 보유한 스레드를 일시 중지한 다음 편향된 잠금을 보유한 스레드가 활성 상태인지 확인합니다. 스레드가 활성 상태가 아닌 경우 개체 헤더는 잠금 해제 상태로 설정됩니다. 스레드가 아직 살아 있으면 편향된 잠금이 있는 스택이 실행되고 편향된 개체의 잠금 기록이 순회됩니다. 스택의 잠금 레코드와 개체 헤더의 Mark Word는 다시 다른 스레드로 편향되거나, 잠금 없는 상태로 돌아가거나 개체를 편향된 잠금으로 부적합한 것으로 표시하고 마지막으로 일시 중지된 스레드를 깨웁니다. 아래 그림의 스레드 1은 바이어스 잠금 초기화 프로세스를 보여주고 스레드 2는 바이어스 잠금 취소 프로세스를 보여줍니다.
바이어스 잠금 설정
바이어스 잠금 끄기: 바이어스 잠금은 Java 6 및 Java 7에서 기본적으로 활성화되어 있지만 필요한 경우 애플리케이션이 시작된 후 몇 초가 지나야 활성화됩니다. JVM 매개변수 -XX: BiasedLockingStartupDelay=0을 사용하여 끌 수 있습니다. 애플리케이션의 모든 잠금이 일반적으로 경쟁 상태에 있다고 확신하는 경우 JVM 매개변수 -XX:-UseBiasedLocking=false를 통해 편향된 잠금을 끄면 기본적으로 경량 잠금 상태로 전환됩니다.
3.2 스핀 잠금
스레드 차단 및 깨우기에는 CPU가 사용자 모드에서 코어 모드로 전환해야 하는 경우가 많아 CPU에 큰 부담을 줍니다. 동시에 우리는 많은 객체 잠금의 잠금 상태가 정수의 자체 증가 작업과 같이 짧은 시간 동안만 지속된다는 것을 알 수 있습니다. 스레드를 차단하고 깨우는 것은 분명히 가치가 없습니다. 짧은 시간 동안 스핀락이 도입되었습니다.
소위 "스핀"은 스레드가 의미 없는 루프를 실행하도록 한 다음 루프가 끝난 후 다시 잠금을 위해 경쟁하는 것입니다. 루프를 계속할 경쟁이 없으면 스레드는 항상 잠금을 유지합니다. 루프 동안 실행 상태에 있지만 JVM 기반 스레드 스케줄링은 시간 조각을 전송하므로 다른 스레드는 여전히 잠금을 적용하고 해제할 수 있습니다.
스핀 잠금은 차단 잠금의 시간과 공간(큐 유지 관리 등) 오버헤드를 절약하지만 장기 스핀은 "바쁨 대기"가 되며 바쁜 대기는 분명히 차단 잠금만큼 좋지 않습니다. 따라서 스핀 수는 일반적으로 10, 100 등의 범위 내에서 제어됩니다. 이 범위를 초과하면 스핀 잠금이 차단 잠금으로 업그레이드됩니다.
스핀 잠금 기간 선택과 관련하여 HotSpot은 스레드 컨텍스트 전환 시간이 가장 좋은 시간이어야 한다고 생각하지만 지금까지 수행되지 않았습니다. 조사 결과 현재 HotSpot은 스핀 사이클 선택 외에도 다음과 같이 몇 가지 CPU 사이클만 일시 중지하고 있습니다.
평균 로드가 CPU보다 적은 경우 계속 회전합니다
(CPU/2)개 이상의 스레드가 회전하는 경우 후속 스레드가 직접 차단됩니다.
회전하는 스레드에서 소유자가 변경된 것을 발견하면 회전 시간(회전 시간) 카운트)이 지연됩니다) 또는 차단에 들어갑니다. CPU가 절전 모드인 경우 회전을 중지하세요.
회전 시간에 대한 최악의 시나리오는 CPU의 저장 지연입니다(CPU A는 일부를 저장합니다). CPU B가 데이터를 학습할 때까지의 직접적인 시간 차이 )
3.3 경량 잠금
경량 잠금
스레드가 동기화 블록을 실행하기 전에 JVM이 먼저 생성합니다. 현재 스레드의 스택 프레임에 있는 스레드는 잠금 레코드의 공간을 저장하고 개체 헤더의 Mark Word를 잠금 레코드에 복사합니다. 이를 공식적으로 Displaced Mark Word라고 합니다. 그런 다음 스레드는 CAS를 사용하여 개체 헤더의 Mark Word를 잠금 레코드에 대한 포인터로 바꾸려고 시도합니다. 성공하면 현재 스레드가 잠금을 획득합니다. 스핀 획득이 계속 실패하면 잠금을 위해 경쟁하는 다른 스레드가 있음을 의미합니다(동일한 잠금을 위해 경쟁하는 두 개 이상의 스레드). 그런 다음 경량 레벨 잠금 장치가 중량 잠금 장치로 확장됩니다.
경량 잠금 잠금 해제
경량 잠금 해제 시 원자 CAS 작업을 사용하여 Displaced Mark Word를 개체 헤더로 다시 교체합니다. 성공하면 동기화 프로세스가 완료되었음을 의미합니다. . 실패하면 다른 스레드가 잠금을 획득하려고 시도했으며 잠금을 해제하는 동안 일시 중지된 스레드를 깨워야 함을 의미합니다. 아래 그림은 두 스레드가 동시에 잠금을 놓고 경쟁하여 잠금 확장이 발생하는 흐름도입니다.
3.4 Heavyweight Lock
Weightlock은 JVM에서는 객체 모니터(Monitor)라고도 합니다. C의 Mutex와 마찬가지로 Mutex의 상호 배제 기능 외에도 Semaphore 기능 구현도 담당합니다. 즉, 최소한 경쟁 잠금을 위한 큐와 신호 차단 큐(대기 큐)를 포함하고 있음을 의미합니다. 전자는 상호 배제를 담당하고 후자는 스레드 동기화에 사용됩니다.
4. 자물쇠의 장단점 비교