2개의 머리카락이 빠지고 휘발성으로 간주 될 수 있습니다.

本来想着快过年了偷个懒休息下,没想到被兄弟们连续催更,没办法,博主暖男嘛,掐着人中也要更,兄弟们卷起来

volatile 키워드는 자바 가상머신이 제공하는 가장 가벼운 동기화 메커니즘이라고 할 수 있지만, 왜 원자성이 아닌 가시성만 보장하는지, 어떻게 명령어 재배열을 비활성화하는지에 대해서는 아직 완전히 이해하지 못한 학생들이 많다.

저를 믿으십시오. 이 기사를 계속 읽으면 핵심 Java 지식 포인트를 확실히 파악하게 될 것입니다.

먼저 두 가지 기능에 대해 이야기해 보겠습니다.

  • 스레드에 대한 메모리의 변수 가시성 보장
  • 명령어 재정렬 비활성화

단어 하나하나 다 알아 모아놓으면 뭉클해

이 두 함수는 일반적으로 우리 Java 개발자가 정확하고 완전하게 이해하기 쉽지 않으므로 많은 학생들이 volatile을 올바르게 사용할 수 없습니다.

가시성 정보

별로 bb, 여기에 코드

public class VolatileTest {
    
    
    private static volatile int count = 0;

    private static void increase() {
    
    
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            new Thread(() -> {
    
    
                for (int j = 0; j < 10000; j++) {
    
    
                    increase();
                }
            }).start();
        }
		// 所有线程累加完成后输出
        while (Thread.activeCount() > 2) Thread.yield();
        System.out.println(count);
    }
}

코드는 이해하기 쉬우며, 10개의 스레드를 열어 동일한 공유 변수 개수를 누적하고, 각 스레드는 1w번 누적합니다.

count를 volatile로 장식하여 메모리에 있는 10개의 스레드까지 count의 가시성을 보장하므로 10개의 스레드가 실행된 후 count 값이 10w가 되어야 합니다.

하지만 여러 번 실행한 결과 예상보다 훨씬 낮은 결과를 얻었습니다
여기에 이미지 설명 삽입
.어떤 링크가 문제인가요?
여기에 이미지 설명 삽입

다음 문장을 들어보셨을 겁니다. volatile은 원자성이 아니라 가시성을 보장합니다.

이 문장이 답이지만 아직도 많은 사람들이 그 수수께끼를 이해하지 못하고 있다

이야기가 길어서 간단히 줄이겠습니다. 요컨대, count++ 작업은 원자적이지 않고 세 단계로 수행됩니다.

  1. 메모리에서 count 값 읽기
  2. 실행 횟수 + 1
  3. 카운트 백의 새 값을 씁니다.

이 문제를 완전히 이해하려면 바이트코드부터 시작해야 합니다.

다음은 증가 방식으로 컴파일한 바이트코드로
여기에 이미지 설명 삽입
, 이해가 안 되어도 상관없으니 한 줄씩 살펴보자.

  1. GETSTATIC: count의 현재 값 읽기
  2. ICONST_1: 스택 맨 위에 상수 1 로드
  3. IADD: 구현에 +1
  4. PUTSTATIC: count의 최신 값 쓰기

ICONST_1 및 IADD는 실제로 실제 ++ 작업입니다.

요점은 volatile이 GETSTATIC 단계에서 스레드가 얻은 값이 최신임을 보장할 수 있지만 스레드가 다음 명령 줄을 실행할 때 다른 스레드가 이 기간 동안 count 값을 수정할 수 있으며 결국 이전 값이 실제 새 값을 덮어씁니다.

나를 이해해

따라서 동시 프로그래밍에서는 volatile에만 의존하여 공유 변수를 수정하는 것은 신뢰할 수 없으며 결국 스레드 안전성을 보장하기 위해 키 메서드를 잠글 필요가 있습니다.

위의 데모와 마찬가지로 약간의 수정으로 진정한 스레드 안전성을 얻을 수 있습니다.

가장 간단한 방법은 동기화된 항목을 증가 메서드에 추가하는 것입니다(동기화된 방법으로 스레드 안전을 달성하는 방법에 대해서는 장황하게 설명하지 않겠습니다. 이전 에 동기화된 기본 구현 원칙에 대해 이야기한 적이 있음 )

    private synchronized static void increase() {
    
    
        ++count;
    }

몇 번 실행해봐 ,
여기에 이미지 설명 삽입
괜찮아?

지금쯤이면 다음 두 가지 사항을 새롭게 이해하게 될 것입니다.

  • volatile은 스레드에 대한 메모리의 변수 가시성을 보장합니다.
  • volatile은 원자성이 아닌 가시성만 보장합니다.

명령어 재배열에 대해

동시 프로그래밍에서 실행 효율성을 향상시키기 위해 CPU 자체와 가상 머신은 명령 재배열을 사용합니다(결과에 영향을 미치지 않는다는 전제 하에 일부 코드는 순서가 잘못 실행됨)

  • cpu 정보: cpu를 활용하기 위해 명령의 실제 실행이 최적화됩니다.
  • 가상 머신 정보: HotSpot vm에서는 실행 효율성을 높이기 위해 JIT(Just-In-Time Compilation) 모드도 명령어 최적화를 수행합니다.

명령어 재배열은 대부분의 시나리오에서 실제로 실행 효율성을 향상시킬 수 있지만 일부 시나리오는 코드 실행 순서에 크게 의존합니다. 이 경우 명령어 재배열을 비활성화해야 합니다.
여기에 이미지 설명 삽입
다음 시나리오에 대한 의사 코드는 "In-depth Understanding of 자바 가상 머신":

설명된 시나리오는 개발 중인 일반적인 구성 읽기 프로세스이지만 일반적으로 구성 파일을 처리할 때 동시성이 발생하지 않으므로 이것이 문제가 될 줄은 몰랐습니다.
초기화된 변수를 정의할 때 휘발성 수정을 사용하지 않으면 명령어 재정렬의 최적화로 인해 스레드 A의 마지막 코드 "initialized=true"가 미리 실행될 수 있다고 상상해 보십시오(여기서는 Java가 의사 코드로 사용되지만 모든 재정렬 최적화는 기계 수준의 최적화 작업이며 조기 실행은 이 명령문에 해당하는 어셈블리 코드가 미리 실행됨을 의미합니다) 스레드 B의 구성 정보를 사용하는 코드에 오류가 있을 수 있고 volatile이 명령을 금지 재정렬하면 이런 일이 일어나지 않도록 할 수 있습니다.

명령어 재배열을 비활성화하려면 변수를 휘발성으로 선언하기만 하면 됩니다.

volatile이 명령어 재정렬을 비활성화하는 방법을 살펴보겠습니다.

"Java Virtual Machine 이해하기"에서 예를 빌리면 이해가 더 쉽습니다.
여기에 이미지 설명 삽입
이것은 싱글톤 모드의 구현입니다. 다음은 바이트 코드의 일부입니다. 빨간색 상자 mov%eax, 0x150(%esi) 는 인스턴스
여기에 이미지 설명 삽입
에 할당 할당 후에 lock addl$0x0, (%esp) 명령도 실행되는 것을 볼 수 있습니다. 핵심은 여기에 있습니다. 이 명령 행은 여기에서 메모리 . 메모리 배리어 이후 , cpu 또는 가상 머신은 명령어가 재배열될 때 메모리 배리어 뒤의 명령어를 메모리 배리어 앞으로 진행할 수 없습니다. 이 구절을 잘 살펴보십시오.


마지막으로 휘발성에 대한 모든 사람의 이해를 심화할 수 있는 질문을 남겨주세요.

자바 코드는 분명히 위에서 아래로 실행되는데 명령어 재배열 문제는 왜 발생하는가?

좋아, 난 끝났어

추천

출처blog.csdn.net/qq_33709582/article/details/122415754