동시 프로그래밍 주제 03-동시 프로그래밍의 원리 (1 부)

머리말

이 섹션에서 시작하여 총 5 개 섹션의 동시 프로그래밍 주제를 입력합니다.

이 섹션의 하이라이트 :

  1. 동기화의 사용 및 원리
  2. 대기 및 알림

동기화 됨

Synchronized는 항상 다중 스레드 동시 프로그래밍에서 베테랑 역할을 해왔으며 많은 사람들이이를 헤비급 잠금이라고 부를 것입니다. 그러나 Java SE 1.6에서 동기화의 다양한 최적화로 인해 어떤 경우에는 그렇게 무겁지 않습니다 .Java SE 1.6에서는 잠금 획득 및 해제의 성능 비용을 줄이기 위해 편향되고 가벼운 잠금 크기도 잠금의 저장 구조 및 업그레이드 프로세스로. 우리는 여전히 이전 케이스를 사용하고, inc 메소드를 수정하기 위해 동기화 된 키워드를 사용합니다. 구현 결과를 살펴보십시오.

public class Demo {
    
    

    private static int count=0;

    public static void inc(){
    
    

        synchronized (Demo.class) {
    
    
            try {
    
    
                Thread.sleep(1);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        for(int i=0;i<1000;i++){
    
    
            new Thread(()->Demo.inc()).start();
        }
        Thread.sleep(3000);
        System.out.println("运行结果"+count);
    }
}

작업 결과 :

실행 결과 1000

동기화의 세 가지 적용 방법

동기화를 잠그는 방법에는 세 가지가 있습니다.

  1. 동기화 코드를 입력하기 전에 현재 인스턴스의 잠금을 얻기 위해 현재 인스턴스의 잠금에 작용하는 인스턴스 메소드 수정.
  2. 정적 메서드는 현재 클래스 개체의 잠금에 작용하며 동기화 코드를 입력하기 전에 현재 클래스 개체의 잠금을 얻어야합니다.
  3. 코드 블록을 수정하고, 잠금 객체를 지정하고, 주어진 객체를 잠그고, 동기화 코드베이스에 들어가기 전에 주어진 객체의 잠금을 얻습니다.

동기화 된 대괄호 뒤의 개체

동기화 된 확장 이후의 객체는 잠금입니다. Java의 모든 객체는 잠금이 될 수 있습니다. 간단히 말해서 객체를 키와 비교합니다. 키를 소유 한 스레드가이 메소드를 실행할 수 있습니다. 키를 얻은 후, 실행 중에 방법,이 열쇠는 당신과 함께 가지고 다니며 오직 하나뿐입니다. 후속 스레드가 현재 메서드에 액세스하려는 경우 키가 없기 때문에 액세스 할 수 없으며 문에서 기다렸다가 이전 스레드가 키를 다시 넣을 때까지만 기다릴 수 있습니다. 따라서 동기화 된 잠긴 객체는 동일해야하며, 다른 객체라면 다른 방의 키라는 의미로 방문자에게 영향을 미치지 않습니다.

동기화 된 바이트 코드 명령어

javap -v를 사용하여 해당 코드의 바이트 코드 명령을 확인합니다. 동기화 블록 구현을 위해 monitorenter 및 monitorexit 명령이 사용됩니다. JMM에 대해 이야기 할 때이 두 명령을 언급했습니다. 암시 적으로 Lock 및 UnLock 작업을 실행했습니다. 원 자성 보장을 제공하는 데 사용됩니다. monitorenter 명령어는 동기화 코드 블록의 시작 부분에 삽입되고 monitorexit 명령어는 동기화 코드 블록의 끝에 삽입됩니다. JVM은 각 monitorenter에 해당하는 monitorexit가 있는지 확인해야합니다.

이 두 명령은 기본적으로 개체의 모니터를 획득하는 것으로,이 프로세스는 배타적이므로 한 번에 동기화 된 개체의 모니터는 하나의 스레드 만 획득 할 수 있습니다.

스레드가 monitorenter 명령을 실행할 때 개체에 해당하는 모니터의 소유권을 획득하려고 시도합니다. 즉, 개체의 잠금을 획득하려고 시도하고, monitorexit를 실행하여 모니터의 소유권을 해제합니다.

동기화 된 잠금의 원리

jdk1.6 이후에는 편향된 잠금, 경량 잠금 및 헤비급 잠금을 포함하여 동기화 된 잠금이 최적화되었습니다. 동기화 된 잠금을 이해하기 전에 두 가지 중요한 개념을 이해해야합니다. 하나는 객체 헤더이고 다른 하나는 모니터입니다.

자바 객체 헤더

핫스팟 가상 머신에서 메모리의 객체 레이아웃은 객체 헤더, 인스턴스 데이터 및 정렬 패딩의 세 영역으로 나뉩니다. Java 객체 헤더는 동기화 된 잠금 객체를 구현하는 기본입니다. 일반적으로 동기화에서 사용하는 잠금 객체는 다음과 같습니다. 스토리지 Java 개체 헤더에 있습니다. 경량 잠금 및 바이어스 잠금의 핵심입니다.

Mawrk Word

Mark Word는 HashCode, GC 생성 기간, 잠금 상태 플래그, 스레드가 보유한 잠금, 편향된 스레드 ID, 편향된 타임 스탬프 등과 같은 객체 자체의 런타임 데이터를 저장하는 데 사용됩니다. Java 객체 헤더는 일반적으로 두 개의 기계 코드를 차지합니다 (32 비트 가상 머신에서 1 개의 기계 코드는 4 바이트, 즉 32 비트).

여기에 사진 설명 삽입

여기에 사진 설명 삽입

소스 코드의 구현

JVM 소스 코드에서 개체 헤더의 정의를 더 깊이 이해하려면 여러 파일 (oop.hpp / markOop.hpp)에주의해야합니다.

oop.hpp, 각 Java 개체에는 JVM 내부에 해당하는 네이티브 C ++ 개체 oop / oopDesc가 있습니다. oop.hpp에서 먼저 살펴보기

oopDesc의 정의

여기에 사진 설명 삽입
_mark는 oopDesc ​​클래스의 맨 위에 선언되어 있으므로이 _mark는 헤더로 간주 할 수 있습니다. 앞서 언급했듯이 헤더는 몇 가지 중요한 상태 및 식별 정보를 저장합니다. markOop.hpp 파일에는 설명을위한 몇 가지 참고 사항이 있습니다. markOop의 메모리 레이아웃

여기에 사진 설명 삽입

감시 장치

모니터 란? 우리는이를 동기화 도구로 이해하거나 동기화 메커니즘으로 설명 할 수 있습니다. 모든 Java 객체는 자연스러운 모니터이며 각 객체의 markOop-> monitor ()는 ObjectMonitor 객체를 저장할 수 있습니다. 소스 코드 수준에서 모니터 개체 분석

  • oop.hpp 아래의 oopDesc ​​클래스는 JVM 개체의 최상위 기본 클래스이므로 각 개체 개체에는 markOop이 포함됩니다.

  • markOop.hpp의 markOopDesc는 oopDesc에서 상속하고 ObjectMonitor
    포인터 객체 를 반환하는 자체 모니터 메서드를 확장 합니다.

  • objectMonitor.hpp, 핫스팟 가상 머신에서 ObjectMonitor 클래스는 모니터를 구현하는 데 사용됩니다.

여기에 사진 설명 삽입

동기화 된 잠금 업그레이드 및 획득 프로세스

개체 헤더와 모니터를 이해 한 후에 동기화 된 잠금의 실현을 분석하는 것은 매우 간단합니다. 앞서 언급했듯이 동기화 된 잠금은 최적화되어 편향된 잠금과 경량 잠금을 도입합니다. 잠금 수준은 잠금 수준이 낮음에서 높음, 잠금 없음-> 편향 잠금-> 경량 잠금-> 중량 잠금으로 점차 업그레이드됩니다.

스핀 락 (CAS)

스핀 잠금은 조건을 충족하지 않는 스레드가 즉시 중단되지 않고 일정 시간 동안 대기하도록하는 것입니다. 자물쇠를 잡고있는 실이 곧 자물쇠를 풀 수 있는지 확인하십시오. 회전하는 방법? 사실 말도 안되는 루프입니다.

프로세서 시간을 점유하여 스레드 전환으로 인한 오버 헤드를 피할 수 있지만 잠금을 보유한 스레드가 곧 잠금을 해제 할 수없는 경우 회전하는 스레드는 아무 작업도하지 않기 때문에 프로세서 리소스를 낭비하게됩니다. 따라서 스핀 대기 시간이나 횟수에 제한이 있으며, 스핀이 정해진 시간을 초과하여 잠금을 획득하지 못한 경우 스레드를 일시 중단해야합니다.

바이어스 잠금

대부분의 경우 잠금은 다중 스레드 경쟁이 없을뿐만 아니라 항상 동일한 스레드에 의해 여러 번 획득되며 스레드가보다 저렴하게 잠금을 획득 할 수 있도록 바이어스 잠금이 도입됩니다. 스레드가 동기화 블록에 액세스하여 잠금을 획득하면 잠금 바이어스의 스레드 ID가 객체 헤더 및 스택 프레임의 잠금 레코드에 저장됩니다. 나중에 스레드는 잠금 및 잠금 해제를 위해 CAS 작업을 수행 할 필요가 없습니다. 동기화 블록에 들어오고 나가기., 객체 헤더의 Mark Word에 저장된 현재 스레드를 가리키는 바이어스 잠금이 있는지 테스트하기 만하면됩니다. 테스트가 성공하면 스레드가 잠금을 획득했음을 의미합니다. 테스트가 실패하면 Mark Word의 바이어스 잠금 마크가 1로 설정되어 있는지 테스트해야합니다 (현재 바이어스 잠금을 나타냄) : 설정되지 않은 경우 CAS 경쟁 잠금을 사용하고, 설정된 경우 CAS를 사용하십시오. 잠금은 현재 스레드를 가리 킵니다.

여기에 사진 설명 삽입

경량 잠금

경량 잠금을 도입하는 주요 목적은 멀티 스레딩 경쟁없이 운영 체제 뮤텍스를 사용하여 기존의 대용량 잠금의 성능 소비를 줄이는 것입니다. 바이어스 잠금 기능이 꺼져 있거나 여러 스레드가 바이어스 잠금을 놓고 경쟁하고 바이어스 잠금이 경량 잠금으로 업그레이드되면 경량 잠금을 획득하려고 시도합니다.
여기에 사진 설명 삽입

헤비 웨이트 락

헤비 웨이트 잠금은 객체 내부의 모니터를 통해 구현됩니다. 모니터의 본질은 기본 운영 체제의 뮤텍스 잠금 구현에 달려 있습니다. 운영 체제에서 스레드 간 전환을 위해서는 사용자 모드에서 커널 모드로 전환해야합니다. 스위칭 비용이 매우 높습니다.
자바 객체 헤더에 대해 이야기 할 때 모니터 객체에 대해 이야기했는데, 핫스팟 가상 머신에서 모니터는 ObjectMonitor 클래스를 통해 구현됩니다. 그의 잠금 획득 프로세스의 구현은 훨씬 간단합니다.

여기에 사진 설명 삽입

대기 및 알림

이전 섹션에서 언급했듯이 wait 및 notify는 스레드를 대기 상태로 만들고 스레드를 깨우는 데 사용되는 두 가지 작업입니다.

사례 발표

여기에서는 데모를 사용하여 대기 및 알림 프로세스를 보여줍니다.

ThreadWait 작성 :

public class ThreadWait extends Thread{
    
    

    private Object lock;

    public ThreadWait(Object lock) {
    
    
        this.lock = lock;
    }

    @Override
    public void run() {
    
    
        synchronized (lock){
    
    
            System.out.println("开始执行 thread wait");
            try {
    
    
                lock.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("执行结束 thread wait");
        }
    }
}

ThreadNotify 작성 :

public class ThreadNotify extends Thread{
    
    

    private Object lock;

    public ThreadNotify(Object lock) {
    
    
        this.lock = lock;
    }

    @Override
    public void run() {
    
    
        synchronized (lock){
    
    
            System.out.println("开始执行 thread notify");
            lock.notify();
            System.out.println("执行结束 thread notify");
        }
    }
}

테스트 클래스 작성 :

public class TestThread {
    
    

    public static void main(String[] args) {
    
    
        Object lock = new Object();
        ThreadWait threadWait = new ThreadWait(lock);
        threadWait.start();
        ThreadNotify threadNotify = new ThreadNotify(lock);
        threadNotify.start();
    }
}

작업 결과 :

실행 스레드 대기
시작 실행 스레드 알림 실행
종료 스레드 알림
실행 종료 스레드 대기

대기 및 알림 원칙

wait 메소드를 호출하면 먼저 모니터 잠금을 획득합니다. 성공한 후 현재 스레드는 대기 상태에 들어가 대기 큐에 들어가 잠금을 해제합니다. 그런 다음 다른 스레드가 notify 또는 notifyall을 호출하면 깨우기를 선택합니다. 대기중인 큐의 스레드입니다. 알림 메서드가 실행 된 후에는 현재 스레드가 여전히 잠금을 유지하고 대기중인 스레드가 잠금을 얻을 수 없기 때문에 스레드가 즉시 깨어나지 않습니다. 현재 스레드가 실행될 때까지 기다렸다가 monitorexit 명령을 눌러야합니다. 즉, 잠금이 해제 된 후 대기 큐의 스레드가 잠금 경쟁을 시작할 수 있습니다.

여기에 사진 설명 삽입

대기 및 알림이 동기화되어야하는 이유는 무엇입니까?
wait 메소드에는 두 가지 의미가 있습니다. 하나는 현재 객체 잠금을 해제하는 것이고, 다른 하나는 현재 스레드가 차단 대기열에 들어가도록하는 것이며, 이러한 작업은 모니터와 관련되어 있으므로 wait는 모니터 잠금을 획득해야합니다.

알림도 마찬가지입니다. 스레드를 깨 웁니다. 깨어나려면 먼저 위치를 알아야합니다. 따라서이 객체의 잠금을 획득하기 위해이 객체를 찾은 다음이 객체의 대기 큐로 이동하여 스레드를 깨울 필요가 있습니다.

끝에 쓰기

이 섹션의 데모 코드 주소 :

https://github.com/harrypottry/ThreadDemo

더 많은 아키텍처 지식을 원하시면 다음 기사 시리즈에 주목하십시오 : The Growth Road of Java Architects

추천

출처blog.csdn.net/qq_34361283/article/details/109562045