>  기사  >  Java  >  Java에서 휘발성 키워드를 사용하는 방법

Java에서 휘발성 키워드를 사용하는 방법

WBOY
WBOY앞으로
2023-04-22 15:55:161239검색

1. 메모리 모델 관련 개념

우리 모두 알고 있듯이 컴퓨터가 프로그램을 실행할 때 각 명령은 CPU에서 실행되며 명령을 실행하는 과정에는 필연적으로 데이터를 읽고 쓰는 작업이 포함됩니다. 프로그램 실행 중 임시 데이터가 메인 메모리(물리적 메모리)에 저장되기 때문에 CPU의 실행 속도가 매우 빠르기 때문에 메모리에서 데이터를 읽고 메모리에 데이터를 쓰는 과정이 다르다는 문제가 있습니다. 명령어를 실행하는 속도는 CPU보다 훨씬 느리기 때문에 언제든지 메모리와 상호작용을 통해 데이터 연산을 수행해야 한다면 명령어 실행 속도가 크게 떨어지게 됩니다. 따라서 CPU에는 캐시가 있습니다.

즉, 프로그램이 실행될 때 작업에 필요한 데이터가 주 메모리에서 CPU 캐시로 복사됩니다. 그런 다음 CPU는 작업이 완료된 후 캐시에서 데이터를 직접 읽고 쓸 수 있습니다. , 캐시의 데이터가 주 메모리로 새로 고쳐집니다. 다음 코드와 같은 간단한 예를 들어보세요.

1i = 나 + 1;

스레드가 이 명령문을 실행하면 먼저 주 메모리에서 i 값을 읽은 다음 복사본을 캐시에 복사합니다. 그런 다음 CPU는 i를 1씩 증가시키는 명령을 실행하고 데이터를 캐시에 씁니다. 마지막으로 캐시에 값을 씁니다. 캐시에 있는 i의 최신 값이 주 메모리로 플러시됩니다.

이 코드를 단일 스레드에서 실행하는 데에는 문제가 없지만 여러 스레드에서 실행하면 문제가 발생합니다. 멀티 코어 CPU에서는 각 스레드가 다른 CPU에서 실행될 수 있으므로 각 스레드는 실행 시 자체 캐시를 갖습니다. (단일 코어 CPU의 경우 실제로 이 문제도 발생하지만 스레드별로 예약됩니다. 별도로 실행되도록 구성됩니다.) . 이 기사에서는 멀티 코어 CPU를 예로 들어 보겠습니다.

예를 들어, 이 코드를 동시에 실행하는 두 개의 스레드가 있습니다. 처음에 i 값이 0이면 두 스레드가 실행된 후 i 값이 2가 되기를 바랍니다. 하지만 과연 그럴 것인가?

다음과 같은 상황이 있을 수 있습니다. 처음에는 두 스레드가 각각 i 값을 읽고 이를 각자의 CPU 캐시에 저장한 다음 스레드 1이 1을 더한 다음 i의 최신 값 1을 메모리에 씁니다. 이때 스레드 2의 캐시에 있는 i의 값은 여전히 ​​0입니다. 1을 더한 후 i의 값은 1이 되고 스레드 2는 i의 값을 메모리에 씁니다.

i의 최종 값은 2가 아니라 1입니다. 이것은 유명한 캐시 일관성 문제입니다. 여러 스레드에서 액세스하는 변수를 일반적으로 공유 변수라고 합니다.

즉, 변수가 여러 CPU에 캐시된 경우(일반적으로 멀티스레드 프로그래밍에서 발생) 캐시 불일치 문제가 발생할 수 있습니다.

캐시 불일치 문제를 해결하기 위해 일반적으로 두 가지 해결책이 있습니다:

1) 버스에 LOCK#을 추가하여

2) 캐시 일관성 프로토콜을 통해

이 두 가지 방법은 하드웨어 수준에서 제공됩니다.

초기 CPU에서는 버스에 LOCK# 잠금을 추가하여 캐시 불일치 문제를 해결했습니다. CPU와 다른 구성 요소 간의 통신은 버스를 통해 이루어지기 때문에 버스에 LOCK#을 추가하면 다른 CPU가 다른 구성 요소(메모리 등)에 접근하는 것을 차단하여 하나의 CPU만이 이를 사용할 수 있다는 의미입니다. 가변 메모리. 예를 들어 위의 예에서 스레드가 i = i +1을 실행하고 이 코드가 실행되는 동안 LCOK# 잠금 신호가 버스에 전송되면 다른 CPU는 이 코드가 완전히 완료될 때까지만 기다릴 수 있습니다. 변수 i가 위치한 메모리에서 변수를 읽어 해당 연산을 수행합니다. 이는 캐시 불일치 문제를 해결합니다.

그러나 위 방법은 버스를 잠그는 동안 다른 CPU가 메모리에 접근할 수 없어 효율성이 떨어지는 문제가 있다.

그래서 캐시 일관성 프로토콜이 나타났습니다. 가장 유명한 것은 각 캐시에 사용되는 공유 변수의 복사본이 일관되도록 보장하는 Intel의 MESI 프로토콜입니다. 핵심 아이디어는 CPU가 데이터를 쓸 때 작동 변수가 공유 변수인 것을 발견하면, 즉 변수의 복사본이 다른 CPU에도 존재한다는 것을 발견하면 다른 CPU에 캐시를 설정하도록 알리는 신호가 전송된다는 것입니다. 따라서 다른 CPU가 이 변수를 읽어야 하고 자신의 캐시에 변수를 캐시하는 캐시 라인이 유효하지 않다는 것을 발견하면 메모리에서 해당 변수를 다시 읽습니다.

2. 동시 프로그래밍의 세 가지 개념

동시 프로그래밍에서는 일반적으로 원자성 문제, 가시성 문제, 순서 문제라는 세 가지 문제에 직면합니다. 먼저 이 세 가지 개념을 자세히 살펴보겠습니다.

1.원자성

원자성: 즉, 하나의 작업 또는 여러 작업이 완전히 실행되고 실행 프로세스가 어떤 요인에 의해 중단되지 않거나 전혀 실행되지 않습니다.

매우 전형적인 예는 은행 계좌 이체 문제입니다:

예를 들어, A 계좌에서 B 계좌로 1,000위안을 이체하려면 A 계좌에서 1,000위안을 빼고 B 계좌에 1,000위안을 추가하는 두 가지 작업이 포함되어야 합니다.

이 두 작업이 원자적이지 않으면 어떤 결과가 나올지 상상해 보세요. A계좌에서 1,000위안을 차감한 후 갑자기 작업이 종료되었다고 가정해보자. 이어 B에서 500위안을 인출했다. 500위안을 인출한 뒤 B계좌에 1000위안을 추가하는 연산을 수행했다. 이로 인해 A계좌에서는 1,000위안이 차감되었으나 B계좌는 이체된 1,000위안을 받지 못하는 결과가 발생합니다.

따라서 이러한 두 작업은 예기치 않은 문제가 발생하지 않도록 원자성이어야 합니다.

동시 프로그래밍에서 동일한 반영의 결과는 무엇입니까?

가장 간단한 예를 들자면, 32비트 변수에 대한 할당 프로세스가 원자적이지 않으면 어떤 일이 일어날지 생각해 보십시오.

1i = 9;

스레드가 이 명령문을 실행하면 32비트 변수에 값을 할당하는 데 하위 16비트에 값을 할당하고 상위 16비트에 값을 할당하는 두 가지 프로세스가 포함된다고 일시적으로 가정하겠습니다.

그런 다음 상황이 발생할 수 있습니다. 낮은 16비트 값을 쓸 때 갑자기 중단되고 이때 다른 스레드가 i 값을 읽은 다음 잘못된 데이터를 읽습니다.

2. 가시성

가시성은 여러 스레드가 동일한 변수에 액세스할 때 한 스레드가 변수 값을 수정하면 다른 스레드가 수정된 값을 즉시 볼 수 있음을 의미합니다.

간단한 예를 들어 다음 코드를 살펴보세요.

//스레드 1

에 의해 실행되는 코드 정수 i = 0;

나 = 10;

//스레드 2

에 의해 실행되는 코드 j = 나;

스레드 1이 CPU1에 의해 실행되면 스레드 2는 CPU2에 의해 실행됩니다. 위의 분석에서 스레드 1이 i = 10이라는 문장을 실행할 때 먼저 i의 초기 값을 CPU1의 캐시에 로드한 다음 이를 10에 할당한 다음 캐시에 i의 값을 할당한다는 것을 알 수 있습니다. CPU1의 값은 10이 되지만, 즉시 메인 메모리에 기록되지는 않습니다.

이때 스레드 2는 j = i를 실행합니다. 먼저 주 메모리로 이동하여 i 값을 읽고 이를 CPU2의 캐시에 로드합니다. 이때 메모리의 i 값은 여전히 ​​0입니다. 그러면 j의 값은 0이 되고 10은 아닙니다.

이는 가시성 문제입니다. 스레드 1이 변수 i를 수정한 후 스레드 2는 스레드 1이 수정한 값을 즉시 볼 수 없습니다.

3. 질서

질서: 즉, 프로그램 실행 순서가 코드 순서대로 실행됩니다. 간단한 예를 들어 다음 코드를 살펴보세요.

정수 i = 0;

부울 플래그 = false;

i = 1; //문장 1

플래그 = true; //문장 2

위 코드는 int형 변수와 boolean형 변수를 정의한 후, 두 변수에 각각 값을 할당하는 코드입니다. 코드 시퀀스로 판단하면 명령문 1이 명령문 2보다 앞에 있습니다. 그러면 JVM이 실제로 이 코드를 실행할 때 명령문 1이 명령문 2보다 먼저 실행된다는 것을 보장할 수 있습니까? 반드시 그런 것은 아닙니다. 왜 그렇습니까? 여기서 명령어 재정렬이 발생할 수 있습니다.

명령어 재정렬이 무엇인지 설명해 보겠습니다. 일반적으로 프로그램 작업의 효율성을 높이기 위해 프로세서는 입력 코드를 최적화할 수 있습니다. 이는 프로그램의 각 명령문의 실행 순서가 코드의 순서와 일치함을 보장하지 않습니다. , 그러나 프로그램의 최종 실행 결과가 코드의 순차적 실행 결과와 일치하는지 확인합니다.

예를 들어, 위의 코드에서 문 1이 먼저 실행되는지 문 2가 먼저 실행되는지는 최종 프로그램 결과에 영향을 미치지 않습니다. 그러면 실행 중에 문 2가 먼저 실행되고 문 1이 나중에 실행될 수 있습니다.

그러나 프로세서가 명령을 재정렬하더라도 프로그램의 최종 결과가 코드의 순차적 실행 결과와 동일하다는 것을 보장한다는 점에 유의해야 합니다. 다음 예를 보세요:

int a = 10; //문장 1

int r = 2; //문장 2

a = a + 3; //문장 3

r = a*a; //문장 4

이 코드에는 4개의 문이 있으므로 가능한 실행 순서는 다음과 같습니다.

따라서 이것이 실행 순서일 가능성이 있습니다: 문 2 문 1 문 4 문 3

불가능합니다. 왜냐하면 프로세서가 순서를 변경할 때 명령어 2가 명령어 1의 결과를 사용해야 하는 경우 프로세서는 명령어 2가 실행되기 전에 명령어 1이 실행되도록 보장하기 때문입니다.

재정렬은 단일 스레드 내에서 프로그램 실행 결과에 영향을 미치지 않지만 멀티스레딩은 어떻습니까? 예를 살펴보겠습니다:

//스레드 1:

context = loadContext(); //문장 1

inited = true; //문장 2

//스레드 2:

동안(!초기화됨){

잠()

}

doSomethingwithconfig(컨텍스트);

위 코드에서 문 1과 문 2는 데이터 종속성이 없으므로 재정렬될 수 있습니다. 재정렬이 발생하면 스레드 1 실행 중에 명령문 2가 먼저 실행되고 스레드 2는 초기화 작업이 완료되었다고 생각한 다음 while 루프에서 뛰어 나와 doSomethingwithconfig(context) 메서드를 실행하지만 이번에는 컨텍스트가 초기화되지 않아 프로그램 오류가 발생합니다.

위에서 볼 수 있듯이 명령어 재정렬은 단일 스레드의 실행에는 영향을 미치지 않지만 스레드의 동시 실행의 정확성에는 영향을 미칩니다.

즉, 동시성 프로그램이 올바르게 실행되기 위해서는 원자성, 가시성, 질서가 보장되어야 합니다. 그 중 하나가 보장되지 않는 한 프로그램이 잘못 실행될 수 있습니다.

3. 자바 메모리 모델

앞서 나는 메모리 모델과 동시 프로그래밍에서 발생할 수 있는 몇 가지 문제에 대해 이야기했습니다. Java 메모리 모델을 살펴보고 Java 메모리 모델이 우리에게 제공하는 보장이 무엇인지, 멀티스레드 프로그래밍을 수행할 때 프로그램 실행의 정확성을 보장하기 위해 Java에서 제공되는 메서드와 메커니즘이 무엇인지 살펴보겠습니다.

Java Virtual Machine 사양은 Java 프로그램이 다양한 플랫폼에서 일관된 메모리 액세스를 달성할 수 있도록 다양한 하드웨어 플랫폼과 운영 체제 간의 메모리 액세스 차이를 보호하기 위해 JMM(Java Memory Model)을 정의하려고 합니다. 그렇다면 Java 메모리 모델은 무엇을 규정합니까? 이는 프로그램의 변수에 대한 액세스 규칙을 정의합니다. 더 넓은 범위에서는 프로그램 실행 순서를 정의합니다. 더 나은 실행 성능을 얻기 위해 Java 메모리 모델은 실행 엔진이 명령 실행 속도를 향상시키기 위해 프로세서의 레지스터나 캐시를 사용하는 것을 제한하지 않으며 컴파일러가 명령을 재정렬하는 것을 제한하지도 않습니다. 즉, Java 메모리 모델에는 캐시 일관성 문제와 명령어 재정렬 문제도 있습니다.

Java 메모리 모델에서는 모든 변수가 주 메모리(앞서 언급한 물리적 메모리와 유사)에 저장되고 각 스레드에는 자체 작업 메모리(이전 캐시와 유사)가 있다고 규정합니다. 스레드에 의한 변수에 대한 모든 작업은 작업 메모리에서 수행되어야 하며 주 메모리에서 직접 작업할 수 없습니다. 그리고 각 스레드는 다른 스레드의 작업 메모리에 액세스할 수 없습니다.

간단한 예를 들자면 Java에서 다음 명령문을 실행합니다.

1i = 10;

실행 스레드는 먼저 자신의 작업 스레드에 변수 i가 있는 캐시 라인을 할당한 다음 이를 주 메모리에 써야 합니다. 값 10을 주 메모리에 직접 쓰는 대신.

그렇다면 Java 언어 자체는 원자성, 가시성 및 순서를 어떻게 보장합니까?

1.원자성

Java에서 기본 데이터 유형의 변수를 읽고 할당하는 작업은 원자 작업입니다. 즉, 이러한 작업은 중단될 수 없으며 실행되거나 실행되지 않습니다.

위의 문장은 간단해 보이지만 이해하기는 그리 쉽지 않습니다. 다음 예를 보세요.

다음 작업 중 원자 작업이 무엇인지 분석해 보세요.

x = 10; //문장 1

y = x; //문장 2

x++; //명령어 3

x = x + 1; //문장 4

언뜻 보면 일부 친구들은 위 네 가지 진술의 연산이 모두 원자 연산이라고 말할 수 있습니다. 실제로 명령문 1만이 원자적 연산이고, 나머지 세 명령문은 원자적 연산이 아닙니다.

명령문 1은 값 10을 x에 직접 할당합니다. 이는 이 명령문을 실행하는 스레드가 값 10을 작업 메모리에 직접 쓰는 것을 의미합니다.

명령문 2에는 실제로 두 가지 작업이 포함되어 있습니다. 먼저 x 값을 읽은 다음 x 값을 작업 메모리에 씁니다. x 값을 읽는 작업과 x 값을 작업 메모리에 쓰는 작업은 원자적 작업입니다. , 그러나 함께 사용하면 원자적 작업이 아닙니다.

마찬가지로 x++ 및 x = x+1에는 x 값 읽기, 1 더하기, 새 값 쓰기라는 3가지 작업이 포함됩니다.

따라서 위의 네 가지 명령문 중 명령문 1의 연산만 원자적입니다.

즉, 단순한 읽기와 할당(그리고 변수에 숫자를 할당해야 하며 변수 간의 상호 할당은 원자적 연산이 아님)만이 원자적 연산입니다.

그러나 여기서 주의할 점이 하나 있습니다. 32비트 플랫폼에서 64비트 데이터를 읽고 할당하려면 두 가지 작업이 필요하며 해당 원자성을 보장할 수 없습니다. 그러나 최신 JDK에서는 JVM이 64비트 데이터를 읽고 할당하는 것도 원자적 작업임을 보장한 것 같습니다.

위에서 볼 수 있듯이 Java 메모리 모델은 기본 읽기 및 할당이 원자성 작업임을 보장합니다. 더 넓은 범위의 작업에 대한 원자성을 달성하려면 동기화 및 잠금을 통해 이를 달성할 수 있습니다. 동기화 및 잠금은 언제든지 하나의 스레드만 코드 블록을 실행하도록 보장하므로 원자성 문제가 없어 원자성을 보장합니다.

2. 가시성

가시성을 위해 Java는 가시성을 보장하기 위해 휘발성 키워드를 제공합니다.

공유 변수가 휘발성으로 수정되면 수정된 값이 즉시 주 메모리로 업데이트됩니다. 다른 스레드가 이를 읽어야 할 경우 메모리에서 새 값을 읽습니다.

일반 공유 변수는 수정된 후 언제 주 메모리에 기록될지 불확실하기 때문에 가시성을 보장할 수 없습니다. 가시성은 보장되지 않습니다.

또한 동기화 및 잠금을 통해 가시성도 보장할 수 있습니다. 동기화 및 잠금은 하나의 스레드만 잠금을 획득하고 동시에 동기화 코드를 실행하도록 보장하며, 변수에 대한 수정 사항은 해제되기 전에 주 메모리로 플러시됩니다. 잠그다. 따라서 가시성이 보장됩니다.

3. 질서

Java 메모리 모델에서는 컴파일러와 프로세서가 명령을 재정렬할 수 있지만 재정렬 프로세스는 단일 스레드 프로그램의 실행에는 영향을 미치지 않지만 다중 스레드 동시 실행의 정확성에는 영향을 미칩니다.

Java에서는 휘발성 키워드를 사용하여 특정 "질서성"을 보장할 수 있습니다. 구체적인 원칙은 다음 섹션에서 설명합니다. 또한 동기화 및 잠금을 통해 질서를 보장할 수 있습니다. 당연히 동기화 및 잠금은 하나의 스레드가 매 순간 동기화 코드를 실행하도록 보장합니다. 이는 스레드가 동기화 코드를 순차적으로 실행하도록 하는 것과 동일하므로 자연스럽게 질서가 보장됩니다.

또한 Java 메모리 모델에는 선천적인 "질서성", 즉 아무런 수단 없이도 보장될 수 있는 질서성이 있습니다. 이를 종종 사전 발생 원칙이라고 합니다. 두 작업의 실행 순서를 사전 발생 원칙에서 추론할 수 없는 경우 순서가 보장되지 않으며 가상 머신이 마음대로 순서를 변경할 수 있습니다.

사전 발생 원칙을 자세히 소개하겠습니다.

프로그램 순서 규칙: 스레드에서는 코드 순서에 따라 앞에 작성된 작업이 뒤에 작성된 작업보다 먼저 발생합니다

잠금 규칙: 잠금 해제 작업이 먼저 발생하고 나중에 동일한 잠금 작업이 발생합니다

휘발성 변수 규칙: 변수에 대한 쓰기 작업은 변수에 대한 후속 읽기 작업보다 먼저 발생합니다

전환 규칙: A 작업이 B 작업보다 먼저 발생하고 B 작업이 C 작업보다 먼저 발생하면 A 작업이 C 작업보다 먼저 발생한다고 결론을 내릴 수 있습니다

스레드 시작 규칙: Thread 개체의 start() 메서드는 이 스레드의 모든 작업에 대해 먼저 발생합니다

스레드 중단 규칙: 스레드 Interrupt() 메서드에 대한 호출은 중단된 스레드의 코드가 인터럽트 이벤트 발생을 감지할 때 먼저 발생합니다

스레드 종료 규칙: 스레드의 모든 작업은 스레드가 종료될 때 먼저 발생합니다. Thread.join() 메서드를 종료하고 Thread.isAlive()

값을 반환하여 스레드가 종료되었음을 감지할 수 있습니다. 객체 마무리 규칙: 객체 초기화는 finalize() 메서드 시작 부분에서 먼저 발생합니다

이 8가지 원칙은 "Java Virtual Machine에 대한 심층적인 이해"에서 발췌한 것입니다.

이 8가지 규칙 중 처음 4가지 규칙이 더 중요하고 마지막 4가지 규칙은 분명합니다.

아래에서 처음 4가지 규칙을 설명해 보겠습니다.

프로그램 순서 규칙의 경우, 프로그램 코드의 실행이 단일 스레드에서 순서대로 나타나는 것으로 이해됩니다. 이 규칙에서는 "앞에 작성된 작업이 뒤에 작성된 작업보다 먼저 발생합니다"라고 언급하지만 이는 가상 머신이 수행할 수 있으므로 프로그램이 실행되는 것으로 나타나는 순서는 코드 순서와 같다는 의미입니다. 프로그램 코드에 대한 작업 순서가 변경되었습니다. 재정렬이 수행되더라도 최종 실행 결과는 프로그램의 순차적 실행 결과와 일치합니다. 이는 데이터 종속성이 없는 명령만 재정렬합니다. 따라서 단일 스레드에서는 프로그램 실행이 순서대로 실행되는 것처럼 보이므로 이를 이해해야 합니다. 실제로 이 규칙은 단일 스레드에서 프로그램 실행 결과의 정확성을 보장하는 데 사용되지만, 다중 스레드에서 프로그램 실행의 정확성을 보장할 수는 없습니다.

두 번째 규칙도 이해하기 쉽습니다. 즉, 단일 스레드이든 다중 스레드이든 동일한 잠금이 잠긴 상태인 경우 잠금 작업을 계속하려면 먼저 잠금을 해제해야 합니다.

세 번째 규칙은 더 중요한 규칙이며 다음 기사에서 중점적으로 다룰 것입니다. 직관적인 설명은 스레드가 변수를 먼저 쓴 다음 스레드가 이를 읽으면 쓰기 작업이 읽기 작업보다 먼저 발생한다는 것입니다.

네 번째 규칙은 실제로 사전 발생 원칙의 전이적 특성을 반영합니다.

4. 휘발성 키워드 심층분석

이전에 많은 이야기를 했지만 모두 휘발성 키워드에 대해 이야기하는 길을 열어주었으므로 다음 주제로 들어가겠습니다.

1.휘발성 키워드의 두 가지 수준의 의미

공유 변수(클래스 멤버 변수, 클래스 정적 멤버 변수)가 휘발성으로 수정되면 두 가지 수준의 의미 체계를 갖게 됩니다.

1) 다른 스레드가 이 변수에 대해 작업할 때 가시성을 보장합니다. 즉, 한 스레드가 변수 값을 수정하면 새 값이 다른 스레드에 즉시 표시됩니다.

2) 명령어 재정렬은 금지됩니다.

먼저 코드를 살펴보겠습니다. 스레드 1이 먼저 실행되고 스레드 2가 나중에 실행되는 경우:

//스레드 1

부울 중지 = false;

동안(!중지){

doSomething();

}

//스레드 2

중지 = 사실;

이 코드는 매우 일반적인 코드 조각이며 많은 사람들이 스레드를 중단할 때 이 표시 방법을 사용할 수 있습니다. 하지만 실제로 이 코드가 완전히 올바르게 실행될까요? 즉, 스레드가 중단됩니까? 반드시 그런 것은 아니지만 대부분의 경우 이 코드는 스레드를 중단할 수 있지만 스레드를 중단할 수 없게 만들 수도 있습니다(이런 가능성은 매우 적지만 일단 이런 일이 발생하면 무한 루프가 발생합니다).

이 코드로 인해 스레드가 중단될 수 없는 이유를 설명하겠습니다. 앞에서 설명했듯이 각 스레드는 실행 중에 자체 작업 메모리를 가지므로 스레드 1이 실행 중일 때 중지 변수의 값을 복사하여 자체 작업 메모리에 넣습니다.

그런 다음 스레드 2가 중지 변수의 값을 변경하지만 이를 주 메모리에 쓸 시간이 되기 전에 스레드 2는 다른 작업을 수행하게 됩니다. 그러면 스레드 1은 스레드 2에 의한 중지 변수의 변경을 알지 못합니다. 계속해서 아래로 내려갑니다.

하지만 휘발성 수정을 사용한 후에는 달라집니다.

첫째: 휘발성 키워드를 사용하면 수정된 값이 즉시 주 메모리에 기록됩니다.

둘째: 휘발성 키워드를 사용하는 경우 스레드 2가 수정하면 스레드 1의 작업 메모리에 있는 캐시 변수 중지의 캐시 라인이 유효하지 않게 됩니다(하드웨어 계층에 반영되면 해당 캐시입니다). CPU의 L1 또는 L2 캐시 라인) 유효하지 않음)

셋째: 스레드 1의 작업 메모리에 있는 캐시 변수 stop의 캐시 라인이 유효하지 않기 때문에 스레드 1은 주 메모리로 이동하여 변수 stop의 값을 다시 읽습니다.

그런 다음 스레드 2가 중지 값을 수정하면(물론 여기에는 스레드 2의 작업 메모리에서 값을 수정한 다음 수정된 값을 메모리에 쓰는 두 가지 작업이 포함됩니다) 변수 중지의 캐시 라인이 중지됩니다. 스레드 1의 작업 메모리에 캐시됩니다. 유효하지 않은 경우 스레드 1이 읽을 때 캐시 라인이 유효하지 않음을 발견합니다. 캐시 라인에 해당하는 주 메모리 주소가 업데이트될 때까지 기다린 다음 최신 값을 읽습니다. 해당 주 메모리에서.

그러면 스레드 1이 읽는 것이 가장 최근의 올바른 값입니다.

2. 휘발성이 원자성을 보장하나요?

위에서 우리는 휘발성 키워드가 작업의 가시성을 보장하지만 변수의 작업이 원자적이라는 것을 휘발성이 보장할 수 있다는 것을 알고 있습니다.

예를 살펴보겠습니다:

공개 수업 테스트 {

공개 휘발성 int inc = 0;

공공 무효 증가() {

inc++;

}

공개 정적 무효 메인(문자열[] args) {

최종 테스트 테스트 = new Test();

for(int i=0;i

새로운 스레드(){

공개 무효 실행() {

for(int j=0;j<1000;j++)

test.increase();

};

}.start();

}

while(Thread.activeCount()>1) //이전 스레드가 모두 실행되었는지 확인하세요

Thread.yield();

System.out.println(test.inc);

}

}

생각해 보세요. 이 프로그램의 결과는 무엇입니까? 어쩌면 어떤 친구들은 그것이 10,000이라고 생각할 수도 있습니다. 그런데 실제로 실행해 보면 매번 결과가 일관되지 않고, 항상 10,000 미만의 숫자임을 알 수 있습니다.

어떤 친구들은 질문할 수도 있습니다. 아니요, 위의 내용은 inc 변수에 대한 자동 증가 작업입니다. 휘발성은 가시성을 보장하므로 각 스레드에서 inc를 증가시킨 후 수정된 값을 볼 수 있으므로 10개의 스레드가 1000을 수행했습니다. 즉, inc의 최종 값은 1000*10=10000이 되어야 합니다.

여기에는 오해가 있습니다. 휘발성 키워드는 가시성이 올바른지 확인할 수 있지만 위 프로그램의 오류는 원자성을 보장하지 못한다는 것입니다. 가시성은 매번 최신 값을 읽는 것만 보장할 수 있지만 휘발성은 변수 작업의 원자성을 보장할 수 없습니다.

앞서 언급했듯이 자동 증가 작업은 원자적이지 않습니다. 여기에는 변수의 원래 값을 읽고 1을 더하고 작업 메모리에 쓰는 작업이 포함됩니다. 즉, 자동 증가 연산의 세 가지 하위 연산이 별도로 실행될 수 있으며 이로 인해 다음과 같은 상황이 발생할 수 있습니다.

특정 순간에 변수 inc의 값이 10이면

스레드 1은 변수에 대해 자동 증가 작업을 수행합니다. 스레드 1은 먼저 변수 inc의 원래 값을 읽은 다음 스레드 1을 차단합니다. 그런 다음 스레드 2는 변수에 대해 자동 증가 작업을 수행하고 스레드 2도 변수 inc의 원래 값을 읽습니다. 스레드 1은 변수 inc만 읽고 변수를 수정하지 않으므로 스레드 2의 작업이 발생하지 않습니다. . 메모리에 있는 캐시 변수 inc의 캐시 라인이 유효하지 않으므로 스레드 2는 inc의 값을 읽기 위해 직접 주 메모리로 이동합니다. 그러면 inc의 값이 10인 것을 확인한 다음 1을 더하고 11을 씁니다. 작업 메모리에 저장하고 마지막으로 주 메모리에 씁니다.

그런 다음 스레드 1은 1을 더합니다. inc 값을 읽었으므로 이 시점에서 스레드 1의 작업 메모리에 있는 inc 값은 여전히 ​​10입니다. 따라서 스레드 1이 inc에 1을 더한 후에는 inc 값이 변경됩니다. 은 11. 이고 작업 메모리에 11을 쓰고 마지막으로 주 메모리에 씁니다.

그런 다음 두 스레드가 각각 자동 증가 작업을 수행한 후 inc는 1씩만 증가합니다.

이것을 설명하고 나면 일부 친구는 질문을 할 수 있습니다. 아니요, 변수가 휘발성 변수를 수정하면 캐시 라인이 유효하지 않다는 것이 보장되지 않습니까? 그러면 다른 스레드가 새 값을 읽습니다. 예, 맞습니다. 이는 위의 발생 전 규칙에 있는 휘발성 변수 규칙이지만 스레드 1이 변수를 읽고 차단된 후에는 inc 값이 수정되지 않는다는 점에 유의해야 합니다. 그런 다음 휘발성은 스레드 2가 메모리에서 변수 inc의 값을 읽는 것을 보장할 수 있지만 스레드 1은 이를 수정하지 않으므로 스레드 2는 수정된 값을 전혀 볼 수 없습니다.

근본 원인은 여기에 있습니다. 자동 증가 작업은 원자적 작업이 아니며 휘발성은 변수에 대한 작업이 원자적임을 보장할 수 없습니다.

효과를 얻으려면 위 코드를 다음 중 하나로 변경하세요.

동기화 사용:

공개 수업 테스트 {

공개 int inc = 0;

공개 동기화 무효 증가() {

inc++;

}

공개 정적 무효 메인(문자열[] args) {

최종 테스트 테스트 = new Test();

for(int i=0;i

새로운 스레드(){

공개 무효 실행() {

for(int j=0;j<1000;j++)

test.increase();

};

}.start();

}

while(Thread.activeCount()>1) //이전 스레드가 모두 실행되었는지 확인하세요

Thread.yield();

System.out.println(test.inc);

}

}

코드 보기

采사용잠금:

공개 수업 테스트 {

공개 int inc = 0;

잠금 잠금 = new ReentrantLock();

공공 무효 증가() {

lock.lock();

시도해 보세요 {

inc++;

} 드디어{

lock.unlock();

}

}

공개 정적 무효 메인(문자열[] args) {

최종 테스트 테스트 = new Test();

for(int i=0;i

새로운 스레드(){

공개 무효 실행() {

for(int j=0;j<1000;j++)

test.increase();

};

}.start();

}

while(Thread.activeCount()>1) //앞면의 线程都执行完

Thread.yield();

System.out.println(test.inc);

}

}

코드 보기

采용AtomicInteger:

공개 수업 테스트 {

공개 AtomicInteger inc = 새로운 AtomicInteger();

공공 무효 증가() {

inc.getAndIncrement();

}

공개 정적 무효 메인(문자열[] args) {

최종 테스트 테스트 = new Test();

for(int i=0;i

새로운 스레드(){

공개 무효 실행() {

for(int j=0;j<1000;j++)

test.increase();

};

}.start();

}

while(Thread.activeCount()>1) //앞면의 线程都执行完

Thread.yield();

System.out.println(test.inc);

}

}

코드 보기

Java 1.5에서는 java.util.concurrent.atomic包下提供了一些原子操작품类,即对基本数据类型의 自增(加1操작), 自减(减1操작), 以及加법操个数) ,减법操작(减一个数)进行了封装,保证这些操작是원원성성작작。atomic是利用CAS来实现数性操작성(비교 및 교환),CAS实际上是利用处理器예를 들어 CMPXCHG는 다음과 같습니다.而处理器执行CMPXCHG指令是一个原子性操작품。

3.휘발성 물질이 있나요?

전면에는 휘발성이 있는 휘발성 문자가 포함되어 있습니다. 휘발성关键字禁止指令重排序有两层의미思:

1) 当程序执行到휘발성 장치는 휘발성 장치를 사용하여 전면적으로 작동하는 장치에서 전체 작업을 수행합니다.后면적 操작동은 还没有进行;

2) 이동이 휘발성이 없는 상태에서 휘발성이 없는 상태에서 휘발성이 있는 상태로 이동하는 것이 불가능합니다. 可能上面说的比较绕,举个简单的例子:

//x、y为비휘발성变weight

//플래그为휘발성变weight

x = 2; //语句1

와이 = 0; //语句2

플래그 = 참; //语句3

x = 4; //语句4

y = -1; //语句5

由于플래그의 양은 휘발성의 양입니다.语句4、语句5后면。但是要注意语句1과 语句2의 顺序, 语句4와 语句5의 顺序是不작용任何保证的。

휘발성이 있는 휘발성 문자를 사용하여 3회, 语句1 및 语句2必定是执行完毕了证, 且语句1 및 语句2의 执行结果对语句3,语句4、语句5是可见的。

那么我们回到前side举的一个例子:

//线程1:

context = loadContext(); //语句1

초기화 = 사실; //语句2

//线程2:

동안(!초기화됨){

잠()

}

doSomethingwithconfig(컨텍스트);

앞장서서 举这个例子의 时候, 提到有可能语句2会에서 1 那么久可能导致context还没被初始化, 而线程2中就使사용未初始性컨텍스트는 导致程序出错。

这里如果는 휘발성 关键字对inated变weight进行修饰,就不会流现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

4.휘발성의 기본 및 장치 관리

전면에는 휘발성이 없습니다. 전면에는 휘발성이 없습니다. 下side这段话摘自 《深入理解Java虚拟机》:

“휘발성이 있는 휘발성 문자와 휘발성이 있는 휘발성 문자를 사용하여 휘발성이 있는 휘발성 문자를 생성합니다. 자물쇠앞에 자물쇠가 달려있습니다. 1) 이전의 位置, 也不会把前면의 指令排到内存屏障면;即지금 执行到内存屏障这句指令时,지금 전면적인 操작업已经전체 부서 구성;

2) 它会强将对缓存的修改操작품立即写入主存;

3) 如果是写操작동, 它会导致其他CPU中对应的缓存行无效。

五.使는 휘발성이 있는 문자를 사용합니다

동기화된 키워드는 여러 스레드가 동시에 코드 조각을 실행하는 것을 방지하므로 프로그램 실행 효율성에 큰 영향을 미칩니다. 휘발성 키워드는 경우에 따라 동기화된 것보다 성능이 더 좋지만 휘발성 키워드는 동기화된 키워드를 대체할 수 없다는 점에 유의해야 합니다. 휘발성 키워드는 작업의 원자성을 보장할 수 없기 때문입니다. 일반적으로 휘발성을 사용하려면 다음 두 가지 조건을 충족해야 합니다.

1) 변수에 대한 쓰기 작업은 현재 값에 의존하지 않습니다

2) 해당 변수는 다른 변수와의 불변성에 포함되지 않습니다

실제로 이러한 조건은 휘발성 변수에 쓸 수 있는 유효한 값이 변수의 현재 상태를 포함한 모든 프로그램 상태와 무관함을 나타냅니다.

실제로 제가 이해한 바에 따르면 위의 두 가지 조건은 휘발성 키워드를 사용하는 프로그램이 동시성 중에 올바르게 실행될 수 있도록 하기 위해 작업이 원자적 작업임을 보장해야 한다는 것입니다.

다음은 Java에서 휘발성이 사용되는 몇 가지 시나리오입니다.

1. 상태 표시 금액

휘발성 부울 플래그 = false;

동안(!플래그){

doSomething();

}

공개 무효 setFlag() {

플래그 = true;

}

휘발성 부울 초기화 = false;

//스레드 1:

컨텍스트 = loadContext();

초기화 = true;

//스레드 2:

동안(!초기화됨){

잠()

}

doSomethingwithconfig(컨텍스트);

2.다시 확인

싱글톤 클래스{

개인용 휘발성 정적 싱글톤 인스턴스 = null;

비공개 싱글턴() {

}

공개 정적 싱글턴 getInstance() {

if(인스턴스==null) {

동기화됨(Singleton.class) {

if(인스턴스==null)

인스턴스 = 새로운 싱글톤();

}

}

인스턴스 반환;

}

}

위 내용은 Java에서 휘발성 키워드를 사용하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제