>  기사  >  Java  >  [Java 동시성 파이팅]------동기화 구현 원리에 대한 심층 분석

[Java 동시성 파이팅]------동기화 구현 원리에 대한 심층 분석

黄舟
黄舟원래의
2017-02-24 09:58:501109검색

제가 처음 Java를 배우기 시작했을 때 멀티스레딩을 접할 때마다 동기화를 사용했던 기억이 납니다. 그 당시에 비해 동기화는 너무나 마술적이고 강력한 것이었습니다. 이는 또한 멀티스레딩 상황에 대한 검증된 솔루션이 되었습니다. 그러나 연구가 진행됨에 따라 동기화는 잠금에 비해 너무 번거로워서 효율적이지 않다고 생각하고 천천히 포기하게 됩니다.

Javs SE 1.6의 다양한 동기화 최적화를 통해 동기화가 그렇게 무거워 보이지는 않는 것이 사실입니다. LZ를 따라 동기화 구현 메커니즘, Java가 이를 최적화하는 방법, 잠금 최적화 메커니즘, 잠금 저장 구조 및 업그레이드 프로세스를 살펴보겠습니다.

구현 원리

동기화할 수 있습니다. 메소드나 코드 블록이 실행 중일 때 동시에 하나의 메소드만 임계 섹션에 들어갈 수 있도록 보장합니다. 또한 공유 변수의 메모리 가시성을 보장할 수도 있습니다

Java의 모든 객체를 사용할 수 있습니다. 잠금으로서, 이것은 동기화의 동기화 구현을 위한 기초입니다:
1. 일반적인 동기화 방법, 잠금은 현재 인스턴스 객체입니다.
2 정적 동기화 방법, 잠금은 현재 클래스의 클래스 객체입니다. 🎜>3. 동기화 방법 블록, 잠금은 괄호 안의 개체

스레드가 동기화된 코드 블록에 액세스할 때 먼저 동기화된 코드를 실행하기 위해 잠금을 획득해야 합니다. 잠금을 해제해야 합니다. 그러면 이 메커니즘을 어떻게 구현합니까? 먼저 간단한 코드를 살펴보겠습니다.

public class SynchronizedTest {
    public synchronized void test1(){

    }    public void test2(){        synchronized (this){

        }
    }
}

javap 도구를 사용하여 생성된 클래스 파일 정보를 보고 동기화 구현을 분석합니다


[Java 동시성 파이팅]------동기화 구현 원리에 대한 심층 분석위에서 볼 수 있듯이 , 동기화 코드 블록은 monitorenter를 사용합니다. monitorexit 명령으로 구현된 동기화 방법(여기서는 JVM의 기본 구현을 살펴봐야 한다는 것이 명확하지 않습니다)은 메서드 수정자의 ACC_SYNCHRONIZED 구현에 의존합니다.

동기화된 코드 블록: monitorenter 명령은 동기화된 코드 블록의 시작 부분에 삽입되고, monitorexit 명령은 동기화된 코드 블록의 끝 부분에 삽입됩니다. 이에 대응하는 monitorexit가 있습니다. 모든 개체에는 연결된 모니터가 있습니다. 모니터를 잡고 있으면 잠긴 상태가 됩니다. 스레드가 monitorenter 명령을 실행할 때 개체에 해당하는 모니터 소유권을 얻으려고 시도합니다. 즉, 개체의 잠금을 얻으려고 시도합니다.
동기화 방법: 동기화 방법은 다음과 같습니다. 일반 메서드 호출로 변환되어 다음과 같은 명령을 반환합니다. Invokevirtual 및 areturn 명령에는 동기화로 수정된 메서드를 구현하기 위한 VM 바이트코드 수준의 특별한 명령이 없습니다. 대신 메서드의 access_flags 필드에 동기화된 플래그 위치가 설정됩니다. Class 파일의 메소드 테이블에서 1로 해당 메소드가 동기화된 메소드임을 나타내며 해당 메소드를 호출하는 객체 또는 해당 메소드가 속한 Class를 사용하여 Klass를 JVM 내부 객체의 잠금 객체로 나타냅니다. (출처: http://www.php.cn/)

분석을 계속하지만 더 깊이 들어가기 전에 Java 객체 헤더와 모니터라는 두 가지 중요한 개념을 이해해야 합니다.

Java 객체 헤더, 모니터

Java 객체 헤더와 모니터는 동기화 구현의 기본! 이 두 가지 개념은 아래에서 자세히 소개됩니다.

Java 객체 헤더

Synchronized에서 사용하는 잠금은 Java 객체 헤더에 저장됩니다. 그러면 Java 객체 헤더란 무엇일까요? 핫스팟 가상 머신의 개체 헤더에는 주로 Mark Word(마크 필드)와 Klass Pointer(유형 포인터)라는 두 가지 데이터 부분이 포함됩니다. 그 중 Klass Point는 객체의 클래스 메타데이터에 대한 포인터입니다. 가상 머신은 이 포인터를 사용하여 객체가 어떤 클래스 인스턴스인지 확인하고 객체 자체의 런타임 데이터를 저장하는 데 사용됩니다. 바이어스 잠금의 핵심이므로 다음에서는 이에 대해 집중적으로 살펴보겠습니다

마크 워드.
Mark Word는 해시 코드(HashCode), GC 생성 기간, 잠금 상태 플래그, 스레드가 보유한 잠금, 편향된 스레드 ID, 편향된 타임스탬프 등과 같은 객체 자체의 런타임 데이터를 저장하는 데 사용됩니다. Java 객체 헤더는 일반적으로 2개의 기계 코드를 차지하지만(32비트 가상 머신에서는 1개의 기계 코드가 4바이트, 즉 32비트임), 객체가 배열 유형인 경우 JVM 가상 머신이기 때문에 3개의 기계 코드가 필요합니다. machine can Java 객체의 크기는 Java 객체의 메타데이터 정보를 통해 결정되지만, 배열의 메타데이터로는 배열의 크기를 확인할 수 없으므로 블록을 사용하여 배열 길이를 기록한다. 다음 그림은 Java 객체 헤더(32비트 가상머신)의 저장 구조이다.
[Java 동시성 파이팅]------동기화 구현 원리에 대한 심층 분석
객체 헤더 정보는 객체가 정의한 데이터와는 무관한 추가 저장 비용이다. 마크워드는 그 자체이지만 가상머신의 공간 효율성을 고려하여 아주 작은 공간에 최대한 많은 데이터를 저장할 수 있도록 고정되지 않은 데이터 구조로 설계되어 객체의 상태에 따라 자체 저장 공간을 재사용하게 됩니다. 즉, Mark Word는 프로그램이 실행됨에 따라 변경되며 변경 상태는 다음과 같습니다(32비트 가상 머신).
[Java 동시성 파이팅]------동기화 구현 원리에 대한 심층 분석

Java 객체 헤더에 대한 간략한 소개입니다. , 다음은 모니터를 살펴보겠습니다.

모니터

모니터란? 동기화 도구로 이해할 수도 있고, 일반적으로 객체로 설명할 수도 있습니다.
모든 것이 객체인 것처럼 모든 Java 객체는 모니터로 탄생합니다. Java 설계에서 모든 Java 객체는 소수의 모니터와 함께 자궁에서 나오기 때문입니다. 누락된 잠금을 내부 잠금 또는 모니터 잠금이라고 합니다.
모니터는 스레드 전용 데이터 구조입니다. 각 스레드에는 사용 가능한 모니터 레코드 목록이 있으며 전역 사용 가능 목록도 있습니다. 잠긴 각 객체는 모니터와 연결됩니다(객체 헤더의 MarkWord에 있는 LockWord는 모니터의 시작 주소를 가리킵니다). 동시에 모니터에는 해당 스레드의 고유 식별자를 저장하는 소유자 필드가 있습니다. 잠금을 소유하며, 이 스레드가 잠금을 소유하고 있음을 나타냅니다. 구조는 다음과 같습니다.
[Java 동시성 파이팅]------동기화 구현 원리에 대한 심층 분석
소유자: 처음에 NULL은 스레드가 현재 모니터 레코드를 소유하고 있지 않음을 의미합니다. 스레드가 잠금을 성공적으로 소유하면 스레드의 고유 ID가 저장됩니다. 잠금이 해제되면 NULL로 설정됩니다.
EntryQ: 시스템 뮤텍스 잠금(세마포어)을 연결하여 모니터 레코드를 잠그지 못한 모든 스레드를 차단합니다.
RcThis: 모니터 기록에서 차단되거나 대기 중인 모든 스레드 수를 나타냅니다.
Nest: 재진입 잠금 계산을 구현하는 데 사용됩니다.
HashCode: 개체 헤더에서 복사된 HashCode 값을 저장합니다(GC 연령도 포함될 수 있음).
후보: 잠금을 해제한 이전 스레드가 모든 스레드를 깨우면 한 번에 하나의 스레드만 성공적으로 잠금을 소유할 수 있으므로 불필요한 차단이나 스레드가 깨어날 때까지 기다리는 것을 방지하는 데 사용됩니다. 스레드는 불필요한 컨텍스트 전환(차단에서 준비로, 경쟁 잠금 실패로 인해 다시 차단)을 발생시켜 심각한 성능 저하를 초래합니다. 후보에는 두 가지 가능한 값만 있습니다. 0은 깨워야 할 스레드가 없음을 의미하고, 1은 잠금을 위해 경쟁하기 위해 후속 스레드를 깨워야 함을 의미합니다.
발췌: Java에서 동기화의 구현 원리 및 응용)
동기화는 매우 무거운 잠금 장치이며 그다지 효율적이지 않다는 것을 알고 있습니다. 동시에 이 개념은 항상 우리 마음 속에 있었습니다. JDK 1.6에서는 동기화가 너무 무겁지 않도록 다양한 최적화가 이루어졌습니다. 그렇다면 JVM은 어떤 최적화 방법을 채택했습니까?

잠금 최적화

jdk1.6은 스핀 잠금, 적응형 스핀 잠금, 잠금 제거, 잠금 조대화, 바이어스 잠금 및 경량화와 같은 잠금 구현에 대한 많은 최적화를 도입했습니다. 잠금 작업의 오버헤드를 줄이기 위해 레벨 잠금과 같은 기술이 사용됩니다.
잠금은 주로 잠금 없음 상태, 편향된 잠금 상태, 경량 잠금 상태, 중량 잠금 상태의 네 가지 상태로 존재하며 치열한 경쟁을 통해 점차 업그레이드됩니다. 잠금은 업그레이드할 수 있지만 다운그레이드할 수는 없습니다. 이 전략은 잠금 획득 및 해제의 효율성을 높이기 위한 것입니다.

自旋锁

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。
何谓自旋锁?
所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。
自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。
自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整;
如果通过参数-XX:preBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。

适应自旋锁

JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。

锁消除

为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。
如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法,Vector的add()方法:

    public void vectorTest(){
        Vector<String> vector = new Vector<String>();        for(int i = 0 ; i < 10 ; i++){
            vector.add(i + "");
        }

        System.out.println(vector);
    }

在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。

锁粗化

我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
在大多数的情况下,上述观点是正确的,LZ也一直坚持着这个观点。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。

경량 잠금

경량 잠금을 도입하는 주요 목적은 다중 스레드 경쟁 없이 운영 체제 뮤텍스를 사용하는 기존의 무거운 잠금으로 인한 성능 소비를 줄이는 것입니다. 바이어스 잠금 기능이 꺼지거나 여러 스레드가 바이어스 잠금을 놓고 경쟁하고 바이어스 잠금이 경량 잠금으로 업그레이드되면 경량 잠금을 획득하려는 시도가 이루어집니다.
잠금 획득
1. 현재 객체가 잠금 해제 상태(해시코드, 0, 01)인지 확인합니다. 그렇다면 JVM은 먼저 스택 프레임에 Lock Record라는 공간을 생성합니다. 잠금 개체의 현재 상태를 저장하기 위한 현재 스레드입니다. Mark Word의 복사본(공식은 이 복사본에 Displaced 접두사를 추가합니다. 즉, Displaced Mark Word)입니다. JVM은 CAS 작업을 사용하여 객체의 마크 워드를 업데이트하여 잠금 기록 수정을 가리키도록 시도합니다. 성공하면 잠금이 완료되었음을 의미하며 잠금 플래그는 00으로 변경됩니다(이 객체가 경량 잠금 상태), 동기화 작업이 실패하면 (3) 단계가 수행됩니다.
3. Mark Word가 현재 스레드의 스택 프레임을 가리키는지 여부를 확인합니다. 그렇다면 이는 현재 스레드가 이미 현재 개체의 잠금을 보유하고 있으며 동기화 코드 블록이 직접 실행된다는 의미입니다. 그렇지 않으면 잠금 개체가 다른 스레드에 의해 선점되었음을 의미하며 이 점에서는 가볍습니다. 레벨 잠금을 중량 잠금으로 확장해야 하며 잠금 플래그가 10이 되고 나중에 대기 중인 스레드가 차단 상태로 전환됩니다.

잠금을 해제합니다 경량 잠금 해제도 마찬가지입니다. CAS 작업을 통해 수행됩니다.
1. 경량 잠금을 획득한 후 Displaced Mark Word에 저장된 데이터를 가져옵니다. 현재 객체의 Mark Word를 가져온 데이터로 바꾸는 CAS 작업. 성공하면 잠금이 성공적으로 해제되었음을 의미하고, 그렇지 않으면 CAS 작업 교체가 실패했음을 의미합니다. 다른 스레드가 잠금을 획득하려고 시도하고 있으며 잠금 스레드를 해제하는 동안 일시 중단된 스레드를 깨워야 합니다.

경량 잠금 장치의 경우 성능 향상의 기본은 "대부분의 잠금 장치는 전체 수명주기 동안 경쟁이 없을 것"입니다. 이 기본이 깨지면 상호 배제의 오버헤드와 더불어, 추가 CAS 작업이 있으므로 멀티 스레드 경쟁의 경우 경량 잠금 장치가 중량 잠금 장치보다 느립니다.

다음 그림은 경량 잠금 장치 획득 및 해제 프로세스를 보여줍니다



바이어스 잠금[Java 동시성 파이팅]------동기화 구현 원리에 대한 심층 분석

바이어스 잠금을 도입하는 주요 목적은 멀티 스레드 경쟁 없이 불필요한 경량 잠금 실행 경로를 최소화하는 것입니다. 위에서 언급한 것처럼 경량 잠금 장치의 잠금 및 잠금 해제 작업에는 여러 CAS 원자 명령이 필요합니다. 그렇다면 편향된 잠금은 어떻게 불필요한 CAS 작업을 줄여줄까요? 마가복음의 구조를 살펴보면 이를 이해할 수 있다. 바이어스 잠금인지, 잠금 ID와 ThreadID만 확인하면 됩니다. 처리 흐름은

잠금 가져오기


1. 바이어스 가능한 상태, 즉 바이어스된 잠금인지 여부 1. 잠금 플래그는 01입니다. 1. 스레드 ID가 현재 스레드 ID인지 테스트합니다. (5) 단계, 그렇지 않으면 (3) 단계를 수행합니다. 1. 스레드 ID가 현재 스레드 ID가 아닌 경우 CAS 작업을 통해 잠금을 경쟁합니다. 경쟁에 성공하면 Mark Word의 스레드 ID를 교체합니다. 현재 스레드 ID, 그렇지 않으면 스레드 (4)를 실행합니다.
4. CAS를 통한 잠금 경쟁에 실패하여 현재 다중 스레드 경쟁 상황이 있음을 증명합니다. 편향된 잠금이 일시 중지되고 편향된 잠금이 경량 잠금으로 업그레이드된 다음 안전 지점에서 차단된 스레드가 계속해서 동기화 코드 블록을 실행합니다.


잠금 해제

바이어스 잠금 해제는 경쟁만이 잠금을 해제하는 메커니즘을 채택합니다. 스레드는 바이어스 해제를 주도하지 않습니다. 잠금은 다른 스레드가 경쟁할 때까지 기다려야 합니다. 편향된 잠금의 철회는 전역 안전 지점(이 시점은 실행 코드가 없는 시점)을 기다려야 합니다. 단계는 다음과 같습니다.

1. 편향된 잠금을 소유한 스레드를 일시 중지하고 잠금 개체가 여전히 잠겨 있는지 확인합니다. 2. 편향된 잠금을 취소하고 잠금 해제 상태(01)로 돌아갑니다. 상태;

다음 그림은 바이어스 잠금의 획득 및 해제 프로세스를 보여줍니다.


무거운 잠금
[Java 동시성 파이팅]------동기화 구현 원리에 대한 심층 분석무거운 잠금이 모니터링됩니다. 모니터의 본질이 기본 운영 체제의 Mutex Lock 구현에 의존하는 객체 모니터 구현 내부 운영 체제의 스레드 간 전환에는 사용자 모드에서 커널 모드로 전환이 필요하며 전환 비용이 매우 높습니다.

위 내용은 [Deadly Java Concurrency]----Synchronized 구현 원리에 대한 심층 분석입니다. 자세한 내용은 PHP 중국어 홈페이지(www.php.cn)를 참고해주세요. )!


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
이전 기사:자바 제네릭다음 기사:자바 제네릭