Java 언어의 휘발 변수는 "가벼운 동기화 된"것으로 간주 될 수 있습니다. 동기화 된 블록과 비교할 때 휘발성 변수는 인코딩 및 런타임 오버 헤드가 덜 필요하지만 달성 할 수있는 기능은 동기화 된 것의 일부일뿐입니다.
잠그다
자물쇠는 상호 배제와 가시성의 두 가지 주요 기능을 제공합니다.
휘발성 변수
휘발 변수는 동기화의 가시성 특성을 가지지 만 원자 특성은 없습니다. 이는 스레드가 휘발성 변수의 최신 값을 자동으로 발견 할 수 있음을 의미합니다.
휘발성 변수는 스레드 안전성을 제공하는 데 사용될 수 있지만 매우 제한된 사용 세트에만 적용될 수 있습니다. 여러 변수 또는 변수의 현재 값과 수정 된 값 사이에는 제약이 없습니다. 따라서 휘발성 만 사용하는 것만으로는 카운터, 뮤텍스 또는 여러 변수와 관련된 불변량이있는 클래스를 구현하기에 충분하지 않습니다 (예 : "시작 <= end").
단순성 또는 확장 성을 위해 잠금 대신 휘발성 변수를 사용하는 경향이 있습니다. 일부 관용구는 잠금 대신 휘발성 변수를 사용할 때 인코딩 및 읽기가 더 쉽습니다. 또한, 휘발성 변수는 자물쇠처럼 스레드 막힘을 유발하지 않으므로 확장 성 문제를 거의 일으키지 않습니다. 경우에 따라 휘발성 변수는 읽기 작업이 쓰기 작업보다 훨씬 큰 경우 잠금보다 성능 이점을 제공 할 수 있습니다.
휘발성 변수를 올바르게 사용하기위한 조건
제한된 경우 잠금 대신 휘발성 변수 만 사용할 수 있습니다. 휘발성 변수 이상적인 스레드 안전을 만들려면 다음 두 조건을 동시에 충족해야합니다.
변수에 대한 쓰기 작업은 현재 값에 의존하지 않습니다.
이 변수는 다른 변수와 불변에 포함되지 않습니다.
실제로, 이러한 조건은 휘발성 변수에 쓸 수있는 이러한 유효한 값이 변수의 현재 상태를 포함하여 모든 프로그램의 상태와 무관하다는 것을 나타냅니다.
첫 번째 조건 제한은 휘발성 변수가 스레드-안전 카운터로 사용되는 것을 방지합니다. 증분 작동 (x ++)은 별도의 작업처럼 보이지만 실제로 원자 적으로 수행 해야하는 일련의 read-modify-write 작업으로 구성된 결합 된 작업이며 휘발성은 필요한 원자 특성을 제공 할 수 없습니다. 올바른 작업을 구현하려면 작업 중 X 값을 일정하게 유지해야하며 휘발 변수에는 불가능합니다. (그러나 값이 단일 스레드에서만 작성되도록 조정되면 첫 번째 조건을 무시할 수 있습니다.)
대부분의 프로그래밍 상황은이 두 조건 중 하나와 충돌하여 휘발성 변수가 동기화 된 스레드 안전에 보편적으로 적용되지 않습니다. 목록 1은 비 스레드-안전 숫자 범위 클래스를 보여줍니다. 불변량이 포함되어 있습니다. 하한은 항상 상한보다 작거나 동일합니다.
예를 들어보세요
아래 예를 봅시다. 우리는 카운터를 구현합니다. 스레드가 시작될 때마다 Counter Inc 메소드가 호출되어 실행 환경에 카운터를 추가하여 JDK 버전 : JDK1.6.0_31, 메모리 : 3G CPU : X86 2.4G
공개 클래스 카운터 {public static int count = 0; public static void inc () {// 여기 지연은 1 밀리 초이며 결과는 {thread.sleep (1); } catch (InterruptedException e) {} count ++; } public static void main (string [] args) {// i ++ 계산을 수행하고 (int i = 0; i <1000; i ++) {new 스레드 (new runnable () {@override public void Run () {counter.inc ();}})에 대한 실제 결과를 확인하기 위해 동시에 1000 개의 스레드를 시작합니다. } // 여기에서 실행되는 각 실행 값은 다를 수 있습니다. }} 실행 결과 : counter.count = 995
실제 작동 결과는 매번 다를 수 있습니다. 기계의 결과는 다음과 같습니다. 실행 결과 : counter.count = 995. 다중 스레드 환경에서 카운트가 결과가 1000이 될 것으로 예상하지 않는다는 것을 알 수 있습니다.
많은 사람들은 이것이 다중 스레드 동시성 문제라고 생각합니다. 이 문제를 피하기 위해 변수 수를 계산하기 전에 휘발성을 추가하면됩니다. 그런 다음 결과가 기대에 부응하는지 확인하기 위해 코드를 수정하고 있습니다.
공개 클래스 카운터 {공개 휘발성 정적 int count = 0; public static void inc () {// 여기 지연은 1 밀리 초이며 결과는 {thread.sleep (1); } catch (InterruptedException e) {} count ++; } public static void main (string [] args) {// 동시에 1000 스레드를 시작하고 i ++ 계산을 수행하고 (int i = 0; i <1000; i ++) {new 스레드 (new Runnable () {@override public void run () {counter.inc ();}})에 대한 실제 결과를 참조하십시오. } // 여기에서 실행되는 각 실행 값은 다를 수 있습니다. 아마도 1000 System.out.println ( "실행 결과 : count.count =" + counter.count); }}실행 결과 : counter.count = 992
작업 결과는 여전히 우리가 예상했던 것보다 1000이 아닙니다. 아래 이유를 분석 해 봅시다
Java Garbage Collection 기사에서 JVM의 순간에 메모리 할당이 설명되어 있습니다. 메모리 영역 중 하나는 JVM 가상 머신 스택입니다. 각 스레드에는 실행될 때 스레드 스택이 있으며 스레드 스택은 스레드 실행 중에 가변 값 정보를 저장합니다. 스레드가 특정 객체의 값에 액세스하면 먼저 객체의 참조를 통해 힙 메모리에 해당하는 변수의 값을 찾은 다음 힙 메모리 변수의 특정 값을 스레드의 로컬 메모리에로드하여 변수 사본을 만듭니다. 그 후, 스레드는 더 이상 객체의 힙 메모리 변수 값과 관계가 없지만 복사 변수의 값을 직접 수정하고 수정 후 특정 순간에 (스레드가 종료되기 전) 스레드 변수의 값을 객체의 힙 변수에 자동으로 씁니다. 이렇게하면 힙 내 물체의 값이 변경됩니다. 다음 그림은이 글의 상호 작용을 설명합니다
메인 메모리에서 현재 작업 메모리로의 EAD 및로드 복사 변수
공유 변수 값을 변경하려면 실행 코드 사용 및 할당
작업 메모리 데이터로 새로 고침 메인 메모리 관련 콘텐츠를 저장 및 쓰기
사용 및 할당이 여러 번 나타날 수 있습니다
그러나 이러한 작업은 원자가 아니며, 즉 읽기 부하 후, 메인 메모리 수 변수가 수정되면 스레드 작업 메모리의 값이로드 된 이후 해당 변경 사항을 유발하지 않으므로 계산 된 결과는 예상과 다릅니다.
휘발성에 의해 수정 된 변수의 경우 JVM 가상 머신은 기본 메모리에서 스레드 작업 메모리로로드 된 값이 최신임을 보장합니다.
예를 들어, 스레드 1과 스레드 2가 읽기 및로드 작업을 수행하고 메인 메모리의 카운트 값이 5라는 것을 알면 최신 값이로드됩니다.
힙 계수가 스레드 1에서 수정되면 주 메모리에 기록되며 기본 메모리의 카운트 변수는 6이됩니다.
스레드 2는 이미 읽기 및로드 작업을 수행 했으므로 메인 메모리 카운트의 변수 값도 작업 후 6으로 업데이트됩니다.
이로 인해 두 개의 스레드가 휘발성 키워드를 제 시간에 수정 한 후 동시성이 발생합니다.