많은 친구들이 키워드 휘발성에 대해 들었을 수도 있고 그것을 사용했을 수도 있습니다. Java 5 이전에는 논란의 여지가있는 키워드였습니다. 프로그램에서 사용하면 예상치 못한 결과가 발생했기 때문입니다. Java 5가 휘발성 키워드가 활력을 되 찾은 후에야만.
휘발성 키워드는 문자 그대로 이해하기 쉽지만 잘 사용하기 쉽지 않습니다. 휘발성 키워드는 Java의 메모리 모델과 관련이 있으므로 휘발성 키를 말하기 전에 먼저 메모리 모델과 관련된 개념과 지식을 이해 한 다음 휘발성 키워드의 구현 원리를 분석하고 휘발성 키워드를 사용하는 몇 가지 시나리오를 제공합니다.
이 기사의 디렉토리 개요는 다음과 같습니다.
1. 메모리 모델의 관련 개념
우리 모두 알다시피, 컴퓨터가 프로그램을 실행할 때 각 명령어는 CPU에서 실행되며 명령을 실행하는 동안 필연적으로 데이터를 읽고 쓰는 것이 포함됩니다. 프로그램 작업 중 임시 데이터는 기본 메모리 (물리적 메모리)에 저장되므로 현재 문제가 있습니다. CPU 실행 속도는 매우 빠르기 때문에 메모리에서 데이터를 읽고 데이터를 작성하는 프로세스는 CPU의 지침 실행보다 훨씬 느립니다. 따라서 언제든지 메모리와의 상호 작용을 통해 데이터 작업을 수행 해야하는 경우 명령 실행 속도가 크게 줄어 듭니다. 따라서 CPU에는 캐시가 있습니다.
즉, 프로그램이 실행될 때 메인 메모리에서 CPU의 캐시로 작동하는 데 필요한 데이터를 복사합니다. 그런 다음 CPU가 계산을 수행하면 캐시에서 직접 데이터를 읽고 데이터를 작성할 수 있습니다. 작업이 완료되면 캐시의 데이터가 기본 메모리로 플러시됩니다. 다음 코드와 같은 간단한 예를 들어 보겠습니다.
i = i + 1;
스레드 가이 명령문을 실행하면 먼저 기본 메모리에서 i의 값을 읽은 다음 사본을 캐시에 복사 한 다음 CPU는 명령어를 실행하여 1에 1을 추가 한 다음 데이터를 캐시에 작성한 다음 캐시의 I의 최신 값을 기본 메모리로 새로 고칩니다.
이 코드는 단일 스레드에서 실행하는 데 아무런 문제가 없지만 멀티 스레드에서 실행할 때 문제가 있습니다. 멀티 코어 CPU에서 각 스레드는 다른 CPU에서 실행될 수 있으므로 각 스레드는 실행할 때 자체 캐시가 있습니다 (단일 코어 CPU의 경우이 문제는 실제로 발생하지만 스레드 예약 형태로 별도로 실행됩니다). 이 기사에서는 멀티 코어 CPU를 예로 들어갑니다.
예를 들어 두 스레드는 동시에이 코드를 실행합니다. I의 값이 처음에 0이면 두 스레드가 실행 된 후 I의 값이 2가되기를 바랍니다. 그러나 이것이 사실입니까?
다음과 같은 상황 중 하나가있을 수 있습니다. 처음에는 두 개의 스레드가 I의 값을 읽고 해당 CPU의 캐시에 저장 한 다음 스레드 1은 1을 추가 한 다음 I의 최신 값을 메모리에 씁니다. 이때 스레드 2의 캐시에서 i의 값은 여전히 0입니다. 1 작업을 수행 한 후 I의 값은 1이고 스레드 2는 i의 값을 메모리에 씁니다.
최종 결과 I의 값은 2가 아닌 1입니다. 이것은 유명한 캐시 일관성 문제입니다. 여러 스레드에서 액세스하는이 변수를 일반적으로 공유 변수라고합니다.
즉, 변수가 여러 CPU (일반적으로 멀티 스레딩 프로그래밍 중에 만 발생)에 캐시 된 경우 캐시 불일치 문제가있을 수 있습니다.
캐시 불일치 문제를 해결하기 위해서는 일반적으로 두 가지 솔루션이 있습니다.
1) 버스에 잠금 잠금 장치를 추가하여
2) 캐시 일관성 프로토콜을 통해
이 두 가지 방법은 하드웨어 수준에서 제공됩니다.
초기 CPU에서는 캐시 불일치 문제가 버스에 잠금# 잠금 장치를 추가하여 해결되었습니다. CPU와 다른 구성 요소 사이의 통신이 버스를 통해 수행되므로 버스에 잠금# 잠금 장치가 추가되면 다른 CPU가 다른 구성 요소 (예 : 메모리)에 액세스 할 수 없으므로 하나의 CPU만이 변수의 메모리를 사용할 수 있습니다. 예를 들어, 위의 예에서 스레드가 i = i +1을 실행하는 경우,이 코드를 실행하는 동안 LCOK# 잠금 신호가 버스로 전송되면 코드가 완전히 실행되기를 기다린 후에 만 다른 CPU는 변수 i가있는 메모리에서 변수를 읽을 수 있으며 해당 작업을 수행 할 수 있습니다. 이것은 캐시 불일치 문제를 해결합니다.
그러나 다른 CPU가 버스 잠금 중에 메모리에 액세스 할 수 없으므로 비 효율성을 초래하기 때문에 위의 방법은 문제가됩니다.
따라서 캐시 일관성 프로토콜이 나타납니다. 가장 유명한 것은 인텔의 MESI 프로토콜로, 각 캐시에 사용되는 공유 변수의 사본이 일관되게 보장합니다. 핵심 아이디어는 다음과 같습니다. CPU가 데이터를 작성할 때, 작동하는 변수가 공유 변수라는 것을 알게되면, 다른 CPU에는 변수의 사본이 있으므로 변수의 캐시 라인을 잘못된 상태로 설정하도록 다른 CPU에 신호를 보냅니다. 따라서 다른 CPU 가이 변수를 읽고 캐시의 변수를 캐시하는 캐시 라인이 유효하지 않다는 것을 알면 메모리에서 다시 읽을 수 있습니다.
2. 동시 프로그래밍의 세 가지 개념
동시 프로그래밍에서, 우리는 일반적으로 원자력 문제, 가시성 문제 및 질서 문제의 다음 세 가지 문제를 겪습니다. 이 세 가지 개념을 먼저 살펴 보겠습니다.
1. 원자력
원자력 : 즉, 하나의 작업 또는 다중 작업이 모두 실행되고 실행 프로세스는 어떠한 요인에 의해서도 중단되지 않거나, 실행되지 않습니다.
매우 전형적인 예는 은행 계좌 이체 문제입니다.
예를 들어, 계정 A에서 계정 B로 1,000 위안을 전송하는 경우 필연적으로 2 개의 작업이 포함됩니다. 계정 A에서 1,000 위안을 빼고 1,000 위안을 계정 B에 추가합니다.
이 두 작업이 원자가 아닌 경우 어떤 결과가 발생할 것인지 상상해보십시오. 계정 A에서 1,000 위안을 빼면 작업이 갑자기 종료됩니다. 그런 다음 500 위안이 B에서 철수 한 후 500 위안을 철회 한 후 1,000 위안을 계정 B에 추가하는 작업은 계정 A가 마이너스 1,000 위안을 가지고 있지만 계정 B는 양도 된 1,000 위안을받지 못했다는 사실로 이어질 것입니다.
따라서 예기치 않은 문제가 없도록하기 위해서는이 두 가지 작업이 원자력이어야합니다.
동시 프로그래밍에 반영 될 결과는 무엇입니까?
가장 간단한 예를 들으려면 32 비트 변수를 할당하는 프로세스가 원자가 아닌 경우 어떻게 될지 생각해보십시오.
i = 9;
스레드 가이 명령문을 실행하는 경우 32 비트 변수의 할당에는 두 가지 프로세스가 포함된다고 가정합니다. 16 비트의 낮은 지정 및 더 높은 16 비트의 할당.
그런 다음 상황이 발생할 수 있습니다. 낮은 16 비트 값이 작성되면 갑자기 중단 되고이 시점에서 다른 스레드가 I의 값을 읽으면 읽기는 잘못된 데이터입니다.
2. 가시성
가시성은 여러 스레드가 동일한 변수에 액세스 할 때, 한 스레드는 변수의 값을 수정하고 다른 스레드는 즉시 수정 된 값을 볼 수 있습니다.
간단한 예는 다음 코드를 참조하십시오.
// 스레드 1에 의해 실행 된 코드는 int i = 0; i = 10입니다. // 스레드 2에서 실행 된 코드는 j = i입니다.
실행 스레드 1이 CPU1이고 실행 스레드 2가 CPU2 인 경우. 위의 분석에서 스레드 1이 문장 i = 10을 실행하면 i의 초기 값이 CPU1의 캐시에로드 된 다음 10의 값을 할당 한 다음 CPU1 캐시에서 i의 값이 10이되지만 즉시 주 메모리에 기록되지는 않습니다.
이때 스레드 2는 J = i를 실행하며 먼저 메인 메모리로 이동하여 i의 값을 읽고 CPU2의 캐시에로드합니다. 메모리에서 i의 값은 여전히 0이므로 j의 값은 10이 아닌 0이됩니다.
이것은 가시성 문제입니다. 스레드 1 후 변수 i를 수정 한 후 스레드 2는 스레드 1에 의해 수정 된 값을 즉시 보지 않습니다.
3. 주문
주문 : 즉, 프로그램 실행 순서는 코드 순서로 실행됩니다. 간단한 예는 다음 코드를 참조하십시오.
int i = 0; 부울 플래그 = 거짓; i = 1; // 문 1 flag = true; // 진술 2
위의 코드는 int- 타입 변수, 부울 유형 변수를 정의한 다음 각각 두 변수에 값을 할당합니다. 코드 시퀀스의 관점에서, 명령문 1은 문서 2 이전입니다. 따라서 JVM이 실제로이 코드를 실행할 때 문서 1 이전에 문서 1이 실행될 것인가? 반드시 그런 것은 아닙니다. 왜? 교육 재정렬이 여기에서 발생할 수 있습니다.
지시 재정의가 무엇인지 설명해 봅시다. 일반적으로 프로그램 작동 효율성을 향상시키기 위해 프로세서는 입력 코드를 최적화 할 수 있습니다. 프로그램의 각 명령문의 실행 순서가 코드의 순서와 일치하는지 확인하지는 않지만 프로그램의 최종 실행 결과와 코드 실행 시퀀스의 결과가 일관되게 보장합니다.
예를 들어, 진술 1 및 문서 2를 실행하는 위 코드에서 첫 번째 프로그램 결과에 최종 프로그램 결과에 영향을 미치지 않으면 실행 프로세스 중에 문 2가 먼저 실행되고 문 1이 나중에 실행될 수 있습니다.
그러나 프로세서가 지침을 다시 주문하지만 프로그램의 최종 결과가 코드 실행 시퀀스와 동일하게 보장합니다. 그렇다면 무엇이 보장됩니까? 다음 예를 살펴 보겠습니다.
int a = 10; // 문 1int r = 2; // 문 2a = a + 3; // 문 3r = a*a; // 문 4
이 코드에는 4 개의 설명이 있으므로 가능한 실행 순서는 다음과 같습니다.
그렇다면 실행 순서가 될 수 있습니까 : 문 2 문장 1 진술 4 진술 3
프로세서가 재정렬 할 때 지침 간의 데이터 의존성을 고려하기 때문에 불가능합니다. 명령 명령어 2가 명령 1의 결과를 사용해야하는 경우, 프로세서는 지침 2 전에 명령 1이 실행되도록합니다.
재주문은 단일 스레드 내에서 프로그램 실행 결과에 영향을 미치지 않지만 멀티 스레딩은 어떻습니까? 아래의 예를 보자 :
// 스레드 1 : context = loadContext (); // state 1inited = true; // state 2 // 스레드 2 : while (! inited) {sleep ()} dosomethingwithconfig (context);위의 코드에서, 진술 1과 2에는 데이터 종속성이 없으므로 재정렬 될 수 있습니다. 재정렬이 발생하면 Stread 1의 실행 중에 문서 2가 먼저 실행되며 Thread 2는 초기화 작업이 완료되었다고 생각하고 While 루프에서 벗어나 DoSomethingswithConfig (Context) 메소드를 실행합니다. 현재 컨텍스트가 초기화되지 않아 프로그램 오류가 발생합니다.
위에서 볼 수 있듯이, 명령 재주문은 단일 스레드의 실행에 영향을 미치지 않지만 동시 실행의 정확성에 영향을 미칩니다.
다시 말해, 동시 프로그램을 올바르게 실행하려면 원자력, 가시성 및 질서를 보장해야합니다. 보장되지 않는 한 프로그램이 잘못 실행 될 수 있습니다.
3. 자바 메모리 모델
메모리 모델과 동시 프로그래밍에서 발생할 수있는 몇 가지 문제에 대해 이야기했습니다. Java 메모리 모델을 살펴보고 Java 메모리 모델이 우리에게 제공하는 보장과 다중 스레드 프로그래밍을 수행 할 때 프로그램 실행의 정확성을 보장하기 위해 Java 메모리 모델이 어떤 방법과 메커니즘을 제공하는지 연구 해 봅시다.
Java Virtual Machine Specification에서는 JMI (Java Memory Model)를 정의하여 다양한 하드웨어 플랫폼과 운영 체제 간의 메모리 액세스 차이를 차단하여 Java 프로그램이 다양한 플랫폼에서 일관된 메모리 액세스 효과를 달성 할 수 있도록합니다. 그렇다면 Java 메모리 모델은 무엇을 규정합니까? 프로그램의 변수에 대한 액세스 규칙을 정의합니다. 더 광범위하게 말하면 프로그램 실행 순서가 정의됩니다. 더 나은 실행 성능을 얻기 위해 Java 메모리 모델은 실행 엔진이 프로세서의 레지스터 또는 캐시를 사용하여 명령 실행 속도를 향상시키지 않으며 컴파일러가 명령어를 재정렬하도록 제한하지 않습니다. 다시 말해, Java 메모리 모델에는 캐시 일관성 문제와 명령 재정의 문제도 있습니다.
Java 메모리 모델은 모든 변수가 메인 메모리 (위에서 언급 한 물리적 메모리와 유사)에 있음을 규정하고 있으며 각 스레드에는 자체 작업 메모리 (이전 캐시와 유사)가 있습니다. 변수의 스레드의 모든 작업은 작업 메모리에서 수행해야하며 기본 메모리에서 직접 작동 할 수 없습니다. 각 스레드는 다른 스레드의 작업 메모리에 액세스 할 수 없습니다.
간단한 예를 들으려면 : Java에서 다음과 같은 진술을 실행하십시오.
i = 10;
실행 스레드는 먼저 변수 i가 자체 작업 스레드에있는 캐시 라인을 할당 한 다음 주 메모리에 작성해야합니다. 메인 메모리에 값 10을 직접 작성하는 대신.
그렇다면 Java 언어 자체가 원자력, 가시성 및 질서를 제공하는 것은 무엇입니까?
1. 원자력
Java에서 기본 데이터 유형의 변수의 읽기 및 할당 작업은 원자 운영입니다. 즉, 이러한 작업은 중단 및 실행 될 수 없습니다.
위의 문장은 단순 해 보이지만 이해하기 쉽지는 않습니다. 다음 예를 참조하십시오 i :
다음 작업 중 원자 운영을 분석하십시오.
x = 10; // 문 1y = x; // 문 2x ++; // 문 3x = x + 1; // 문 4
언뜻보기에 일부 친구들은 위의 네 가지 진술의 작전이 모두 원자 운영이라고 말할 수 있습니다. 실제로, 진술 1만이 원자 작용이며, 다른 세 문화 중 어느 것도 원자 연산이 아닙니다.
명령문 1은 값 10을 직접 할당합니다. 이는 스레드 가이 문을 실행하고 값 10을 작업 메모리에 직접 씁니다.
진술 2에는 실제로 2 개의 작업이 포함됩니다. 먼저 x의 값을 읽은 다음 x의 값을 작업 메모리에 기록해야합니다. X의 값을 읽고 X의 값을 작업 메모리에 쓰는 두 가지 작업은 원자 연산이지만, 원자 연산은 아닙니다.
마찬가지로 X ++ 및 X = X+1에는 3 개의 작업이 포함됩니다. X의 값을 읽고 1 추가 작업을 수행하고 새 값을 작성하십시오.
따라서 위의 4 문장에서 문서 1의 작동 만 원자입니다.
다시 말해서, 간단한 판독 및 할당 만 (그리고 숫자는 변수에 할당되어야하며 변수 간의 상호 할당은 원자 연산이 아닙니다)는 원자 연산입니다.
그러나 여기에는 주목해야 할 사항이 있습니다. 32 비트 플랫폼에서 64 비트 데이터의 읽기 및 할당은 두 가지 작업을 통해 완료해야하며 원자력을 보장 할 수 없습니다. 그러나 최신 JDK에서 JVM은 64 비트 데이터의 읽기 및 할당도 원자 작동임을 보장 한 것으로 보입니다.
위에서부터 Java 메모리 모델은 기본 판독 및 할당이 원자 연산임을 보장합니다. 더 넓은 범위의 작업의 원자력을 달성하려면 동기화 및 잠금을 통해 달성 할 수 있습니다. 동기화 된 및 잠금은 언제든지 코드 블록을 실행할 수 있으므로 원자력 문제가 없으므로 원자력이 보장됩니다.
2. 가시성
가시성을 위해 Java는 가시성을 보장하기 위해 휘발성 키워드를 제공합니다.
공유 변수가 휘발성에 의해 수정되면 수정 된 값이 즉시 기본 메모리로 업데이트되도록하고 다른 스레드가 읽어야 할 때 메모리의 새 값을 읽습니다.
그러나 일반 공유 변수는 수정 된 후 정상 공유 변수가 기본 메모리에 기록 된시기가 확실하지 않기 때문에 가시성을 보장 할 수 없습니다. 다른 스레드가 읽을 때 원래의 오래된 값은 여전히 메모리에있을 수 있으므로 가시성을 보장 할 수 없습니다.
또한 동기화 된 및 잠금도 가시성을 보장 할 수 있습니다. 동기화 및 잠금은 하나의 스레드만이 동시에 잠금을 획득하고 동기화 코드를 실행하도록 할 수 있습니다. 잠금 장치를 출시하기 전에 변수의 수정이 기본 메모리로 새로 고침됩니다. 따라서 가시성을 보장 할 수 있습니다.
3. 주문
Java 메모리 모델에서 컴파일러와 프로세서는 지침을 재정렬 할 수 있지만 재정렬 프로세스는 단일 스레드 프로그램의 실행에 영향을 미치지 않지만 멀티 스레드 동시 실행의 정확성에 영향을 미칩니다.
Java에서는 휘발성 키워드를 통해 특정 "OrderLine"을 보장 할 수 있습니다 (특정 원칙은 다음 섹션에서 설명합니다). 또한 동기화 및 잠금을 사용하여 순서를 보장 할 수 있습니다. 분명히 동기화 된 및 잠금은 매 순간에 동기화 코드를 실행하는 스레드가 있는지 확인합니다. 이는 스레드가 순서대로 동기화 코드를 실행하도록하는 것과 동일하며, 이는 자연스럽게 순서를 보장합니다.
또한 Java 메모리 모델에는 선천적 인 "OrderLine"이 있습니다. 즉, 어떤 수단도없이 보장 될 수 있으며, 이는 일반적으로 발생하기 전에 발생합니다. 두 작업의 실행 순서가 이전의 원칙에서 발생할 수없는 경우, 순서 성을 보장 할 수 없으며 가상 머신은 마음대로 다시 주문할 수 있습니다.
발생하기 전에 발생하는 원칙 (우선 순위 발생 원칙)을 소개하겠습니다.
이 8 가지 원칙은 "Java Virtual Machines에 대한 심층적 인 이해"에서 발췌됩니다.
이 8 가지 규칙 중에서 처음 4 개의 규칙이 더 중요하지만 마지막 4 가지 규칙은 모두 명백합니다.
아래의 첫 번째 4 가지 규칙을 설명해 봅시다.
프로그램 순서 규칙의 경우, 내 이해는 단일 스레드에서 프로그램 코드의 실행이 주문되는 것 같습니다. 이 규칙은 "전면에 작성된 작업이 뒷면에 작성된 작업에서 먼저 발생합니다"라고 언급하지만, 가상 머신이 프로그램 코드를 다시 주문할 수 있기 때문에 프로그램이 코드 시퀀스에서 실행되는 순서가되어야합니다. 재정렬이 수행되지만 최종 실행 결과는 프로그램 순차 실행과 일치하며 데이터 종속성이없는 지침 만 재정렬합니다. 따라서 단일 스레드에서 프로그램 실행은 순서대로 실행되는 것으로 보이며, 이는주의해서 이해되어야합니다. 실제로,이 규칙은 단일 스레드에서 프로그램의 실행 결과의 정확성을 보장하는 데 사용되지만, 다중 스레드 방식으로 프로그램의 정확성을 보장 할 수는 없습니다.
두 번째 규칙은 또한 이해하기가 더 쉽습니다. 즉, 동일한 잠금이 잠긴 상태에있는 경우 잠금 작동이 계속되기 전에 해제되어야합니다.
세 번째 규칙은 비교적 중요한 규칙이며 나중에 논의 될 것입니다. 직관적으로, 스레드가 먼저 변수를 쓰고 스레드가 읽는 경우, 읽기 작업에서 먼저 쓰기 작업이 발생합니다.
네 번째 규칙은 실제로 발생하기 전에 발생하는 원칙이 전이적이라는 것을 반영합니다.
4. 휘발성 키워드의 심층 분석
나는 이전에 많은 것들에 대해 이야기했지만 실제로는 휘발성 키워드를 말하는 방법을 포장하고 있으므로 주제에 접근합시다.
1. 휘발성 키워드의 2 층 의미
공유 변수 (클래스 멤버 변수, 클래스 정적 멤버 변수)가 휘발성에 의해 수정되면 두 개의 의미론이 있습니다.
1)이 변수를 작동 할 때 다른 스레드의 가시성을 보장하십시오. 즉, 하나의 스레드는 특정 변수의 값을 수정 하고이 새 값은 즉시 다른 스레드에 표시됩니다.
2) 지침을 재정렬하는 것은 금지되어 있습니다.
먼저 코드를 살펴 보겠습니다. 스레드 1이 먼저 실행되고 스레드 2가 나중에 실행 된 경우 :
// 스레드 1boolean spop = false; while (! stop) {dosomething ();} // thread 2stop = true;이 코드는 매우 일반적인 코드이며, 많은 사람들이 스레드를 방해 할 때이 마크 업 방법을 사용할 수 있습니다. 그러나 실제로이 코드가 완전히 올바르게 실행됩니까? 스레드가 중단됩니까? 반드시 그런 것은 아닙니다. 아마도 대부분의 경우,이 코드는 스레드를 방해 할 수 있지만 스레드가 중단되지 않을 수도 있습니다 (이 가능성은 매우 작지만 일단 발생하면 죽은 루프가 발생합니다).
이 코드로 인해 스레드가 방해하지 않는 이유를 설명해 봅시다. 앞에서 설명한 바와 같이, 각 스레드는 작동 중에 자체 작업 메모리가 있으므로 스레드 1이 실행될 때 정지 변수의 값을 복사하여 자체 작업 메모리에 넣습니다.
그런 다음 스레드 2가 정지 변수의 값을 변경하지만 메인 메모리에 쓸 시간이 없으면 스레드 2는 다른 작업을 수행하면 스레드 2의 스톱 변수 변경에 대해 알지 못하므로 계속 루프됩니다.
그러나 휘발성으로 수정 한 후에는 다르게됩니다.
첫째 : 휘발성 키워드를 사용하면 수정 된 값이 주 메모리에 즉시 기록되도록합니다.
둘째 : 휘발성 키워드를 사용하는 경우 스레드 2가 수정되면 스레드 1의 작업 메모리의 캐시 변수 정지의 캐시 라인이 유효하지 않습니다 (하드웨어 레이어에 반영되면 CPU의 L1 또는 L2 캐시의 해당 캐시 라인이 잘못되었습니다).
셋째 : 스레드 1의 작업 메모리에서 캐시 변수 정지의 캐시 라인이 유효하지 않으므로 스레드 1은 변수 중지의 값을 다시 읽을 때 기본 메모리에서 읽습니다.
그런 다음 스레드 2가 정지 값을 수정하면 (물론, 여기에 2 개의 작업이 있으며, 스레드 2의 작업 메모리의 값을 수정 한 다음 메모리에 수정 된 값을 작성) 스레드 1의 작업 메모리에서 캐시 변수 중지의 캐시 라인이 잘못되었습니다. 스레드 1을 읽으면 캐시 라인이 유효하지 않다는 것을 알게됩니다. 캐시 라인의 해당 기본 메모리 주소가 업데이트 될 때까지 기다린 다음 해당 메인 메모리에서 최신 값을 읽습니다.
스레드 1이 읽는 것은 최신 올바른 값입니다.
2. 휘발성이 원자력을 보장합니까?
위에서, 우리는 휘발성 키워드가 운영의 가시성을 보장하지만 변수의 작업이 원자인지 확인할 수 있다는 것을 알고 있습니다.
아래의 예를 보자 :
공개 클래스 테스트 {공개 휘발성 int Inc = 0; 공개 무효 증가 () {inc ++; } public static void main (String [] args) {최종 테스트 = new test (); for (int i = 0; i <10; i ++) {new Thread () {public void run () {for (int j = 0; j <1000; j ++) test.increase (); }; }.시작(); } while (ride.ActiveCount ()> 1) // 이전 스레드가 thread.yield ()를 완료했는지 확인하십시오. System.out.println (test.inc); }}이 프로그램의 출력 결과가 무엇인지 생각해보십시오. 어쩌면 어떤 친구들은 그것이 10,000이라고 생각합니다. 그러나 실제로 달리기는 각 실행 결과가 일치하지 않으며 10,000보다 적습니다.
어떤 친구들에게는 질문이있을 수 있습니다. 잘못되었습니다. 위의 내용은 변수 Inc에서 자체 증가 작업을 수행하는 것입니다. 휘발성은 가시성을 보장하기 때문에 각 스레드에서 Inc의 자체 증가 후 수정 된 값을 다른 스레드에서 볼 수 있습니다. 따라서 10 개의 스레드가 각각 1000 개의 작업을 수행하므로 INC의 최종 값은 1000*10 = 10000이어야합니다.
여기에는 오해가 있습니다. 휘발성 키워드는 가시성을 보장 할 수 있지만 위의 프로그램은 원자력을 보장 할 수 없기 때문에 잘못되었습니다. 가시성은 최신 값을 매번 읽을 수 있지만 휘발성은 변수 작동의 원자력을 보장 할 수는 없습니다.
앞에서 언급했듯이 자동 증가 작업은 원자가 아닙니다. 여기에는 변수의 원래 값을 읽고, 추가 작업을 수행하고, 작업 메모리에 쓰는 것이 포함됩니다. 즉, 자체 증가 작업의 세 가지 하위 운영은 별도로 수행 될 수 있으며, 이는 다음과 같은 상황으로 이어질 수 있습니다.
특정 시간에 variable Inc의 값이 10이면
스레드 1은 변수에서 자체 증가 작업을 수행합니다. 스레드 1은 먼저 변수 Inc의 원래 값을 읽은 다음 스레드 1이 차단됩니다.
그런 다음 Thread 2는 변수에서 자체 증가 작업을 수행하고 Thread 2는 변수 inc의 원래 값을 읽습니다. Thread 1은 변수 Inc에서만 읽기 작업을 수행하고 변수를 수정하지 않으므로 Thread 2의 Cache Inc Cache 변수 Inc의 캐시 라인이 유효하지 않습니다. 따라서 스레드 2는 INC의 값을 읽기 위해 메인 메모리로 직접 이동합니다. INC의 값이 10이라는 것이 발견되면 1을 추가하는 작업을 수행하고 작업 메모리에 11을 기록하고 마지막으로 메인 메모리에 씁니다.
그런 다음 스레드 1은 추가 작업을 수행합니다. INC의 값이 읽히기 때문에 현재 스레드 1의 Inc 값은 현재 10이므로 스레드 1이 추가 된 후 Inc의 값은 11이고, 11은 메모리를 작성하고 마지막으로 메인 메모리에 씁니다.
그런 다음 두 스레드가 자체 점수 작업을 수행 한 후 INC는 1 만 증가합니다.
이것을 설명하면 일부 친구들에게 질문이있을 수 있습니다. 잘못되었습니다. 휘발성 변수를 수정할 때 변수가 캐시 라인을 무효화 할 것이라고 보장하지 않습니까? 그러면 다른 스레드는 새로운 값을 읽습니다. 예, 이것은 맞습니다. 이것은 위의 전기 규칙에서 휘발성 변수 규칙이지만, 스레드 1이 변수를 읽고 차단되면 Inc 값을 수정하지 않습니다. 그런 다음 휘발성 2가 메모리에서 변수 inc의 값을 읽도록 보장 할 수 있지만 스레드 1은 수정되지 않았으므로 스레드 2는 수정 된 값을 전혀 볼 수 없습니다.
근본 원인은 자동화 조작이 원자 연산이 아니며 변수의 모든 작업이 원자임을 보장 할 수 없기 때문입니다.
위의 코드를 다음 중 하나로 변경하면 효과를 달성 할 수 있습니다.
동기화 된 사용 :
공개 수업 테스트 {public int Inc = 0; public synchronized void allok () {inc ++; } public static void main (String [] args) {최종 테스트 = new test (); for (int i = 0; i <10; i ++) {new Thread () {public void run () {for (int j = 0; j <1000; j ++) test.increase (); }; }.시작(); } while (ride.ActiveCount ()> 1) // 이전 스레드가 thread.yield ()를 완료했는지 확인하십시오. System.out.println (test.inc); }} 잠금 사용 :
공개 수업 테스트 {public int Inc = 0; 잠금 잠금 = 새로운 재 렌트 링크 (); public void alkins () {lock.lock (); {inc ++; } 마침내 {lock.unlock (); }} public static void main (String [] args) {최종 테스트 = new test (); for (int i = 0; i <10; i ++) {new Thread () {public void run () {for (int j = 0; j <1000; j ++) test.increase (); }; }.시작(); } while (ride.ActiveCount ()> 1) // 이전 스레드가 실행되었는지 확인하십시오. System.out.println (test.inc); }} Atomicinteger 사용 :
공개 클래스 테스트 {public atomicinteger inc = new atomicinteger (); public void alkins () {inc.getAndIncrement (); } public static void main (String [] args) {최종 테스트 = new test (); for (int i = 0; i <10; i ++) {new Thread () {public void run () {for (int j = 0; j <1000; j ++) test.increase (); }; }.시작(); } while (ride.ActiveCount ()> 1) // 이전 스레드가 실행되었는지 확인하십시오. System.out.println (test.inc); }}일부 원자 운영 클래스는 Java.util.concurrent.자가 1.5의 원자 패키지, 즉 자체 점수 (1 조작 추가), 자체 재구성 (1 개의 작업 추가), 추가 작업 (추가) 및 차축 작업 (추가)의 기본 데이터 형식 (추가)에 따라 제공됩니다. Atomic은 CAS를 사용하여 원자 작업을 구현합니다 (비교 및 스왑). CAS는 실제로 프로세서에서 제공하는 CMPXCHG 명령어를 사용하여 구현되며 프로세서는 CMPXCHG 명령어를 실행합니다.
3. 휘발성이 질서를 보장 할 수 있습니까?
앞에서 언급했듯이 휘발성 키워드는 명령어 재정렬을 금지 할 수 있으므로 휘발성은 어느 정도 순서를 보장 할 수 있습니다.
휘발성 키워드의 재정비를 금지하는 두 가지 의미가 있습니다.
1) 프로그램이 휘발성 변수의 읽기 또는 쓰기 작업을 실행할 때, 이전 작업에 대한 모든 변경 사항이 이루어져야하며 결과는 이미 후속 작업에 표시됩니다. 후속 작업은 아직 이루어지지 않았어야합니다.
2) 명령 최적화를 수행 할 때, 휘발성 변수에 액세스 한 명령문은 그 뒤에 배치 될 수 없으며, 휘발 변수에 따른 진술은 그 앞에 배치 할 수 없습니다.
위에서 언급 한 것은 약간 혼란 스러울 수 있으므로 간단한 예를 들어보십시오.
// x와 y는 비 휘발성 변수입니다. // 플래그는 휘발성 변수 x = 2입니다. // 문 1y = 0; // 문 2Flag = true; // 문 3x = 4; // 문 4y = -1; // 문 5
플래그 변수는 휘발성 변수이므로 명령어 재정렬 프로세스를 수행 할 때 문서 3은 문서 1과 2 앞에 배치되지 않으며 문서 3 및 문서 4 및 5 이후에 배치되지 않습니다. 그러나 문서 1 및 문서 2 및 명령문 5의 순서는 보장되지 않습니다.
또한, 휘발성 키워드는 명령문 3이 실행되면 문서 1 및 문서 2가 실행되어야하며 문 1 및 문서 2의 실행 결과가 문 3, 문 4 및 문 5로 표시되도록 할 수 있습니다.
따라서 이전 예로 돌아가 봅시다.
// 스레드 1 : context = loadContext (); // state 1inited = true; // state 2 // 스레드 2 : while (! inited) {sleep ()} dosomethingwithconfig (context);이 예를 제시했을 때, 문서 2가 문 1 이전에 실행될 수 있다고 언급 했으므로 컨텍스트가 초기화되지 않을 수 있으며 스레드 2는 초기화되지 않은 컨텍스트를 사용하여 작동 오류를 초래할 수 있습니다.
입력 변수가 휘발성 키워드로 수정되면 문 2가 실행되면 컨텍스트가 초기화되었는지 확인하기 때문에이 문제는 발생하지 않습니다.
4. 휘발성의 원리와 구현 메커니즘
휘발성 키워드의 일부 사용에 대한 이전 설명은 시작되었습니다. Let’s discuss how volatile ensures visibility and prohibits instructions to reorder.
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
五.使用volatile关键字的场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
下面列举几个Java中使用volatile的几个场景。
1.状态标记量
volatile boolean flag = false; while(!flag){ doSomething();} public void setFlag() { flag = true;} volatile boolean inited = false;//线程1:context = loadContext(); inited = true; //线程2:while(!inited ){sleep()}doSomethingwithconfig(context);2.double check
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); }} return instance; }}참조 :
《Java编程思想》
《深入理解Java虚拟机》
위는이 기사의 모든 내용입니다. 모든 사람의 학습에 도움이되기를 바랍니다. 모든 사람이 wulin.com을 더 지원하기를 바랍니다.