Java 동시 프로그래밍 원칙 1(원자성, 가시적 행, 질서, 휘발성, 동기화)

1. 원자성:

1.1 Java에서 스레드 안전성을 달성하는 방법은 무엇입니까?

다중 스레드 작업에서 공유 데이터 문제.
잠그다:

  • 비관적 잠금: 동기화, 잠금
  • 낙관적 잠금: CAS

비즈니스 상황에 따라 ThreadLocal을 선택하여 각 스레드가 자체 데이터를 사용하도록 할 수 있습니다.

1.2 CAS 기본 구현

Java의 관점에서 CAS는 Java 수준에서 네이티브 메서드를 볼 수 있습니다.
당신은 비교와 교환을 알게 될 것입니다:

  • 먼저 값이 예상 값과 일치하는지 비교하고 일치하면 true를 교환하고 반환합니다.
  • 먼저 값이 예상 값과 일치하는지 비교하고 일치하지 않으면 교환하지 않고 false를 반환합니다.

Unsafe 클래스에서 제공하는 CAS 작업으로 이동할 수 있습니다.

4개의 매개변수: 객체, 속성의 메모리 오프셋, oldValue, newValue

이미지.png

네이티브는 네이티브 종속성 라이브러리 C++에서 메서드를 직접 호출하는 것입니다.

https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/prims/unsafe.cpp

이미지.png

https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp

CAS 하단에 멀티코어 운영체제라면 잠금 명령어를 추가해야 한다.

cmpxchg는 한 줄 명령이고 더 이상 분할할 수 없기 때문에 단일 코어를 추가할 필요가 없습니다.

이미지.png

cmpxchg가 어셈블리 명령임을 알면 기본 CPU 하드웨어는 비교 및 ​​교환 (cmpxchg)을 지원하며 cmpxchg는 원자성을 보장하지 않습니다. (cmpxchg의 동작은 분할할 수 없는 명령입니다)

그래서 CPU가 멀티코어인지 판단하고, 멀티코어라면 잠금 명령어가 추가된다.

잠금 명령을 CPU 수준의 잠금으로 이해할 수 있습니다. 일반적으로 잠금의 세분성은 캐시 라인 수준의 잠금입니다. 물론 버스 잠금 도 있지만 비용이 너무 높고 CPU가 상황에 따라 선택하십시오.

1.3 카스

ABA: ABA가 문제가 될 필요는 없습니다! ++, -에만 존재하는 연산이 있기 때문에 ABA 문제가 있어도 결과에 영향을 주지 않습니다!

스레드 A: A1 - B2에서 값을 변경할 것으로 예상

스레드 B: B2 - A3에서 기대하는 가치

스레드 C: A1 - C4에서 값이 변경될 것으로 예상됨

원자성 측면에서 스레드 안전성을 보장할 수 없습니다.

솔루션은 간단하며 Java 측에서 이미 제공됩니다.

이미지.png

인간의 관점에서 값을 수정할 때 버전 번호를 지정하십시오.

JUC에서 제공하는 AtomicStampedReference를 구현할 수 있습니다.

너무 많은 스핀:

너무 많은 스핀은 많은 CPU 리소스를 차지합니다! 자원 낭비.

  • 동기화된 방향: 여러 CAS 실패 후 너무 많은 CPU 리소스를 차지하지 않도록 스레드(WAITING)를 일시 중단합니다!
  • LongAdder 방향: 여기에는 segmented locks과 유사한 형태의 솔루션이 있다(업종에 따라 제약이 있음) 전통적인 AtmoicLong은 메모리의 유일한 값에 대해 ++하는 것이다 LongAdder는 많은 값을 생성했다 마다스레드

원자성은 AQS를 학습한 후 이해할 수 있는 솔루션이라는 하나의 속성에 대해서만 보장됩니다 . ReentrantLock은 AQS를 기반으로 구현되며, AQS는 CAS를 기반으로 핵심 기능을 구현합니다.

1.4 네 가지 참조 유형 + ThreadLocal

네 가지 참조 유형:

  • 강력한 참조: User xx = new User(); xx는 강력한 참조이며 참조가 아직 있는 한 GC는 이를 재활용하지 않습니다!

  • 소프트 참조: SofeReference가 참조하는 객체는 소프트 참조이며, 메모리 공간이 부족하면 객체에 대한 소프트 참조 포인트만 재활용됩니다. 일반적으로 캐싱에 사용됩니다 .

    SoftwareReference xx = new SoftwareReference (new User);
    
    User user = xx.get();
    
  • 약한 참조: WeakReference가 참조하는 객체는 일반적으로 약한 참조이며, GC가 실행되는 한 약한 참조가 가리키는 객체만 재활용됩니다. ThreadLocal을 보면 메모리 누수 문제를 해결할 수 있습니다.

  • 팬텀 참조: PhantomReference의 기능은 가비지 수집기의 활동을 추적하여 개체를 수집하는 것입니다.GC 프로세스 중에 PhantomReference가 발견되면 GC는 참조를 ReferenceQueue에 넣고 프로그래머가 직접 처리합니다. 프로그래머가 ReferenceQueue.pull() 메서드를 호출하면 참조된 ReferenceQueue를 제거한 후 참조 개체가 비활성화되어 참조된 개체를 재활용할 수 있습니다.

둘째, 눈에 보이는 선:

2.1 자바 메모리 모델

명령을 처리할 때 CPU는 데이터를 가져옵니다. 우선순위는 L1에서 L2, L3이며, 없는 경우 메인 메모리에서 가져와야 합니다. JMM은 가시성을 보장하기 위해 CPU와 메인 메모리 사이를 조정하고 효과적인 순차적 작업

JVM의 메모리 구조가 아니라 아무것도 아닙니다! ! ! ! (자바 메모리 모델)

이미지.png,
CPU 코어는 CPU 코어(레지스터)입니다.

캐시는 CPU의 캐시로 CPU의 캐시는 L1(스레드 전용), L2(코어 전용), L3(멀티 코어 공유)로 나뉩니다.

JMM은 Java 메모리 모델의 핵심이며 가시성 및 순서는 이 구현을 기반으로 합니다.

주 메모리 JVM은 힙 메모리입니다.

2.2 가시성 확보 방안

가시성이란 무엇입니까: 가시성은 변수의 변경 사항이 스레드 간에 표시되는지 여부를 나타냅니다.

Java 수준에서 가시성을 보장하는 방법에는 여러 가지가 있습니다.

  • 휘발성 , 휘발성 기본 데이터 유형을 사용하면 CPU가 데이터를 작동할 때마다 주 메모리에 직접 읽고 쓸 수 있습니다.
  • 동기화됨, 동기화됨의 메모리 의미 체계는 잠금이 획득된 후 이전에 작동된 데이터가 표시되도록 보장할 수 있습니다.
  • 잠금(CAS-휘발성)은 또한 CAS 또는 휘발성 변수의 작동 후 이전에 작동된 데이터가 표시되도록 할 수 있습니다.
  • 최종, 그것은 상수이며 이동할 수 없습니다 ~~

2.3 휘발성 수정 참조 데이터 유형

먼저 결과에 대해 이야기하겠습니다. 먼저 참조 데이터 유형의 휘발성 수정은 참조 데이터 유형의 주소가 표시되도록만 보장할 수 있으며 내부 속성이 표시되도록 보장하지 않습니다.

그러나 이 결론은 핫스팟에서만 실현될 수 있으며 다른 버전의 가상 머신으로 변경하면 효과가 다를 수 있습니다. volatile은 참조 데이터 유형을 수정하지만 JVM은 이러한 종류의 작업을 전혀 표준화하지 않았으며 다른 가상 머신 공급업체에서 자체적으로 구현할 수 있습니다.

2.4 MESI 프로토콜에서 여전히 휘발성이 있는 이유는 무엇입니까?

MESI는 CPU 캐시 일관성을 위한 프로토콜이며 대부분의 CPU 제조업체는 MESI를 기반으로 캐시 일관성 효과를 달성했습니다.

CPU에는 이미 MESI 프로토콜이 있으며 휘발성이 약간 중복되지 않습니다! ?

우선 두 형제는 충돌하지 않습니다. 하나는 CPU 하드웨어 수준의 일관성이고 다른 하나는 Java에서 JMM 수준의 일관성입니다.

MESI 프로토콜에는 고정된 메커니즘이 있습니다. 휘발성 선언 여부에 관계없이 이 메커니즘을 기반으로 캐시의 일관성(가시성)을 보장합니다. 동시에 MESI 프로토콜이 없으면 휘발성에 몇 가지 문제가 있지만 다른 솔루션(버스 잠금, 시간 비용이 너무 높음, 버스가 잠겨 있으면 하나의 CPU 코어만 일하고 있는).

MESI는 프로토콜, 계획 및 인터페이스이며 CPU 제조업체에서 구현해야 합니다.

CPU가 MESI를 가지고 있는데 왜 여전히 휘발성이 있는 걸까요?당연히 MESI 프로토콜에 문제가 있습니다. MESI는 멀티 코어 CPU의 전용 캐시 간 가시성을 보장하지만 CPU는 레지스터의 데이터를 L1에 직접 써야 한다는 의미는 아닙니다. 대부분의 x86 아키텍처 CPU에서는 레지스터와 L1 , 레지스터 값은 저장 버퍼에 떨어질 수 있지만 L1에는 들어가지 않아 캐시 불일치가 발생할 수 있습니다. x86 아키텍처를 사용하는 CPU 외에도 arm 및 power CPU에는 캐시 일관성에 어느 정도 영향을 미치는 로드 버퍼와 유효하지 않은 대기열도 있습니다!

MESI 프로토콜과 휘발성은 MESI가 CPU 수준에 있고 많은 CPU 제조업체가 다른 구현을 가지고 있기 때문에 충돌하지 않으며 CPU 아키텍처의 일부 세부 사항도 영향을 미칩니다. 예를 들어 Store Buffer는 레지스터 쓰기에 영향을 미칩니다. 캐시 불일치가 발생합니다. 휘발성의 맨 아래 계층은 어셈블리 잠금 명령을 생성합니다.이 명령은 메인 메모리에 대한 강제 쓰기를 필요로 하며, 가시성 목적을 달성하기 위해 Store Buffer 캐시를 무시할 수 있으며 MESI 프로토콜은 다른 캐시 라인을 무효화하는 데 사용됩니다. *

2.5 휘발성 가시성의 기본 구현

휘발성의 맨 아래 계층은 어셈블리 잠금 명령을 생성합니다.이 명령은 메인 메모리에 대한 강제 쓰기를 필요로 하며, 가시성 목적을 달성하기 위해 Store Buffer 캐시를 무시할 수 있으며 MESI 프로토콜은 다른 캐시 라인을 무효화하는 데 사용됩니다.

3. 규칙적인 고주파 문제:

3.1 주문 문제는 무엇입니까

싱글톤 모드의 지연 메커니즘에는 이러한 문제가 있습니다.

스레드 안전을 보장하기 위해 게으른 사람들은 일반적으로 DCL을 사용합니다.

그러나 DCL만으로는 여전히 문제가 발생할 가능성이 있습니다.

스레드는 반 초기화된 개체를 작동시킬 수 있으며 NullPointException이 발생할 가능성이 매우 높습니다.

(객체의 세 부분 초기화, 공간 열기, 내부 속성 및 참조에 대한 포인터 초기화)

자바에서 .java를 .class로 컴파일할 때 JIT를 기반으로 최적화를 하고 명령어의 순서를 조정해 실행 효율을 높일 예정이다.

CPU 수준에서 일부 실행은 실행 효율성을 개선하기 위해 재정렬됩니다.

이 명령을 조정하면 일부 특수 작업에서 문제가 발생할 수 있습니다.

3.2 휘발성 주문의 기본 구현

휘발성에 의해 수정된 속성은 컴파일 도중 컴파일 전후에 메모리 장벽을 추가합니다 .

SS: 배리어 이전의 읽기 및 쓰기 작업은 후속 작업을 수행하기 전에 완료되어야 합니다.

SL: 후속 읽기 작업을 수행하기 전에 배리어 이전의 모든 쓰기 작업을 완료해야 합니다.

LL: 후속 읽기 작업을 수행하기 전에 장벽 이전의 읽기 작업을 완료해야 합니다.

LS: 후속 쓰기 작업을 수행하기 전에 장벽 이전의 읽기 작업을 완료해야 합니다.

이미지.png

이 메모리 배리어는 JDK에 의해 지정되며 그 목적은 휘발성 수정 속성이 명령어 재정렬 문제를 갖지 않도록 하는 것입니다.

Volatile은 JMM 수준에 있으며 JIT가 재정렬되지 않도록 하는 것은 이해할 수 있지만 CPU는 이를 어떻게 구현합니까?

이 문서를 확인하세요: https://gee.cs.oswego.edu/dl/jmm/cookbook.html

이미지.png

다른 CPU는 메모리 장벽에 대한 특정 지원을 제공합니다.예를 들어 x86 아키텍처는 내부적으로 LS, LL 및 SS를 구현했으며 SL만 지원합니다.

mfence가 어떻게 지원하는지 다시 확인하려면 openJDK로 이동하세요. 사실, 명령 재정렬 문제를 해결하기 위해 여전히 mfence 내부 잠금에 의해 맨 아래 계층이 지정됩니다.

이미지.png

四、동기화:

4.1 동기화 잠금 업그레이드 프로세스

잠금은 객체이며 어느 것이든 괜찮습니다. Java의 모든 객체는 잠금입니다.

잠금 없음(익명 바이어스), 바이어스 잠금, 경량 잠금, 중량 잠금

잠금 해제(익명 편향) : 정상적인 상황에서 new의 객체는 잠금 해제 상태입니다. 편향된 잠금에 지연이 있기 때문에 JVM을 시작하는 4초에는 편향된 잠금이 없지만 편향된 잠금 지연 설정을 해제하면 새 개체는 익명의 바이어스입니다.

바이어스 잠금 : 특정 스레드가 이 잠금 리소스를 획득하면 이때 바이어스 잠금이 되며 바이어스 잠금은 스레드의 ID를 저장합니다.

편향된 잠금이 업그레이드되면 편향된 잠금 해제를 트리거합니다.편향된 잠금 해제는 안전한 지점까지 기다려야 합니다.예를 들어 GC 중에 편향된 잠금 해제 비용이 너무 높기 때문에 기본적으로 편향된 잠금은 처음에 지연됩니다.

안전한 지점:

  • GC
  • 메서드가 반환되기 전에
  • 메서드 호출 후
  • 비정상적인 위치
  • 루프의 끝

Lightweight Lock : 여러 쓰레드간 경쟁이 있을 때 Lightweight Lock으로 업그레이드 필요 (No Lock에서 Lightweight Lock으로 바로 변경 가능, Biased Lock에서 Lightweight Lock으로 업그레이드도 가능) ), 경량 잠금의 효과는 CAS를 기반으로 잠금 리소스를 획득하려고 시도하는 것입니다.

Heavyweight Lock: Heavyweight Lock이 있으면 말할 것도 없고, Lock을 잡고 있는 쓰레드가 있으면 경쟁하는 다른 쓰레드가 정지된다.

4.2 동기화된 잠금 조대화 및 잠금 제거

잠금 조대화(잠금 확장): (JIT 최적화)

while(){
   sync(){
      // 多次的获取和释放,成本太高,优化为下面这种
   }
}
//----
sync(){
   while(){
       //  优化成这样
   }
}

잠금 제거: 동기화에는 공유 리소스가 없으며 잠금 경쟁이 없습니다.JIT가 컴파일되면 잠금 지침이 직접 최적화됩니다.

4.3 동기화된 상호 배제의 원리

편향된 잠금: 객체 헤더의 MarkWord에서 스레드 ID를 확인하여 현재 스레드인지 확인하고 그렇지 않은 경우 CAS로 변경해 보십시오. 그렇다면 잠금 리소스를 얻은 것입니다.

경량 잠금: 객체 헤더의 MarkWord에 있는 Lock Record 포인터가 현재 스레드의 가상 머신 스택을 가리키는지 확인하고, 그렇다면 잠금을 사용하여 업무를 실행하고, CAS가 아니면 수정을 시도한다. 실패하면 다시 시도하십시오 Heavyweight 잠금 장치로 업그레이드하십시오.

Heavyweight lock: 객체 헤더에서 MarkWord가 가리키는 ObjectMonitor를 확인하여 소유자가 현재 스레드인지 확인합니다. 그렇지 않은 경우 ObjectMonitor의 EntryList에 던져넣고 대기하고 스레드를 일시 중단하고 깨우기를 기다립니다.

이미지.png

4.4 Object에서 wait가 왜 메서드인가요?

대기 메서드를 실행하려면 동기화 잠금을 유지해야 합니다.
동기화 잠금은 모든 객체가 될 수 있습니다.
동시에 wait 메서드가 실행되어 동기화 잠금이 유지될 때 잠금 리소스를 해제합니다.
둘째, wait 메소드는 ObjectMonitor를 동작시켜야 하고, ObjectMonitor의 동작은 잠금 자원을 보유한다는 전제하에 동작해야 하며, 현재 쓰레드는 WaitSet 대기 풀로 던져진다.

같은 방식으로 notify 메소드는 WaitSet 대기 풀의 스레드를 EntryList로 던져야 합니다.ObjectMonitor를 소유하지 않은 경우 어떻게 합니까!

클래스 잠금은 클래스를 기반으로 하며, 클래스는 클래스 잠금으로 사용됩니다.
객체 잠금은 객체 잠금으로 새 객체입니다.

추천

출처blog.csdn.net/lx9876lx/article/details/129112793