Java에서 휘발성과 동기화의 차이점


233

Java volatile에서 synchronized(this)블록으로 변수를 선언 하고 항상 변수에 액세스하는 것과의 차이점이 궁금합니다 .

이 기사 http://www.javamex.com/tutorials/synchronization_volatile.shtml 에 따르면 말할 것이 많고 많은 차이점이 있지만 약간의 유사점이 있습니다.

이 정보에 특히 관심이 있습니다.

...

  • 휘발성 변수에 대한 액세스는 절대로 차단할 가능성이 없습니다. 우리는 간단한 읽기 또는 쓰기 만 수행하므로 동기화 된 블록과 달리 잠금을 유지하지 않습니다.
  • 휘발성 변수에 액세스하면 잠금이 유지되지 않기 때문에 원자 업데이트읽기-쓰기-쓰기 를 수행 하려는 경우에는 적합하지 않습니다 ( "업데이트를 놓칠"준비가되지 않은 경우).

read-update-write 는 무엇을 의미 합니까? 쓰기도 업데이트가 아니거나 단순히 업데이트 가 읽기에 의존하는 쓰기임을 의미 합니까?

무엇 volatile보다도 synchronized블록을 통해 변수에 액세스하는 대신 변수를 선언하는 것이 더 적합한시기는 언제 입니까? volatile입력에 의존하는 변수 에 사용하는 것이 좋습니다 ? 예를 들어, render렌더링 루프를 통해 읽히고 keypress 이벤트로 설정된 변수 가 있습니까?

답변:


383

나사산 안전 에는 두 가지 측면 이 있다는 것을 이해하는 것이 중요합니다 .

  1. 실행 제어
  2. 메모리 가시성

첫 번째는 코드가 실행될 때 (명령이 실행되는 순서 포함)와 동시에 실행될 수 있는지 여부를 제어하는 ​​것과 관련이 있으며, 두 번째는 수행 된 메모리의 효과가 다른 스레드에 표시 될 때와 관련이 있습니다. 각 CPU는 CPU와 주 메모리 사이에 여러 레벨의 캐시를 가지고 있기 때문에 스레드가 주 메모리의 개인 사본을 확보하고 작업 할 수 있기 때문에 다른 CPU 또는 코어에서 실행중인 스레드는 특정 시점에 "메모리"를 다르게 볼 수 있습니다.

를 사용 synchronized하면 다른 스레드 가 동일한 객체에 대한 모니터 (또는 잠금) 얻지 못 하므로 동일한 객체 에 대한 동기화로 보호 된 모든 코드 블록이 동시에 실행되지 않습니다. 또한 동기화 "어쩌면 이전의"메모리 장벽을 만들어 일부 스레드가 잠금을 해제하는 시점까지 수행 된 모든 작업이 잠금 을 획득하기 전에 동일한 잠금 을 획득 다른 스레드에 나타나는 것처럼 보일 수 있도록 메모리 가시성 제약 조건 을 발생시킵니다. 실제적인 용어로, 현재 하드웨어에서는 일반적으로 모니터를 획득 할 때 CPU 캐시가 플러시되고 해제 될 때 주 메모리에 쓰며 둘 다 (상대적으로) 비쌉니다.

사용 volatile힘 한편, 휘발성 변수에 대한 모든 액세스는 (판독 또는 기록)을 효과적으로 CPU 캐시로부터 휘발성 변수를 유지하는, 메인 메모리에 발생한다. 변수의 가시성이 정확해야하고 액세스 순서가 중요하지 않은 일부 작업에 유용 할 수 있습니다. 사용 volatile도 치료를 변경 long하고 double원자로 그들에게 접근을 필요로; 일부 오래된 하드웨어에서는 최신 64 비트 하드웨어가 아닌 잠금이 필요할 수 있습니다. Java 5+에 대한 새로운 (JSR-133) 메모리 모델에서 휘발성의 의미는 메모리 가시성 및 명령 순서와 관련하여 동기화되는 수준만큼 거의 강화되었습니다 ( http://www.cs.umd.edu 참조) . /users/pugh/java/memoryModel/jsr-133-faq.html#volatile). 가시성을 위해 휘발성 필드에 대한 각 액세스는 절반의 동기화처럼 작동합니다.

새로운 메모리 모델에서는 여전히 휘발성 변수를 서로 재정렬 할 수 없습니다. 차이점은 이제 더 이상 일반 필드 액세스를 재정렬하기가 쉽지 않다는 것입니다. 휘발성 필드에 쓰는 것은 모니터 릴리즈와 동일한 메모리 효과를 가지며 휘발성 필드에서 읽는 것은 모니터가 획득하는 것과 동일한 메모리 효과를 갖습니다. 실제로, 새로운 메모리 모델은 다른 필드 액세스 (휘발성 또는 비 휘발성)에 의한 휘발성 필드 액세스의 순서를 다시 지정하는 데보다 엄격한 제약을 가하기 때문에 휘발성 필드에 A쓸 때 스레드에서 볼 수있는 것은 읽을 때 f스레드에 표시됩니다 .Bf

- JSR 133 (자바 메모리 모델) 자주 묻는 질문

따라서 현재 JMM 아래의 두 가지 형태의 메모리 장벽은 명령어 재정렬 장벽을 유발하여 컴파일러 나 런타임이 장벽을 넘어 명령을 재정렬하지 못하게합니다. 이전 JMM에서는 휘발성이 재정렬을 방해하지 않았습니다. 메모리 장벽을 제외하고 부과되는 유일한 제한은 특정 스레드 에 대해 명령이 명령이 명령 순서에 나타난 순서대로 정확하게 실행 된 경우와 동일하다는 것입니다. 출처.

휘발성의 한 가지 용도는 공유되지만 변경 불가능한 객체를 즉시 재생하는 것입니다. 다른 많은 스레드가 실행주기의 특정 시점에서 객체를 참조합니다. 하나는 게시 된 객체가 다시 생성 된 객체를 사용하기 시작하려면 다른 스레드가 필요하지만 전체 동기화에 대한 추가 오버 헤드가 필요하지 않으며 수행자 경합 및 캐시 플러시가 필요합니다.

// Declaration
public class SharedLocation {
    static public SomeObject someObject=new SomeObject(); // default object
    }

// Publishing code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
//       someObject will be internally consistent for xxx(), a subsequent 
//       call to yyy() might be inconsistent with xxx() if the object was 
//       replaced in between calls.
SharedLocation.someObject=new SomeObject(...); // new object is published

// Using code
private String getError() {
    SomeObject myCopy=SharedLocation.someObject; // gets current copy
    ...
    int cod=myCopy.getErrorCode();
    String txt=myCopy.getErrorText();
    return (cod+" - "+txt);
    }
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.

읽기-쓰기-쓰기 질문에 대해 구체적으로 말하십시오. 다음과 같은 안전하지 않은 코드를 고려하십시오.

public void updateCounter() {
    if(counter==1000) { counter=0; }
    else              { counter++; }
    }

이제 updateCounter () 메소드가 비동기 화되면 두 개의 스레드가 동시에 입력 할 수 있습니다. 발생할 수있는 많은 순열 중 하나는 thread-1이 counter == 1000에 대한 테스트를 수행하여 true를 찾은 다음 일시 중단한다는 것입니다. 그런 다음 thread-2는 동일한 테스트를 수행하고 또한 사실을 확인하고 일시 중단됩니다. 그런 다음 thread-1이 재개되고 카운터를 0으로 설정합니다. 그러면 thread-2가 재개되고 다시 thread-1에서 업데이트를 놓쳤으므로 카운터를 0으로 설정합니다. 위에서 설명한 것처럼 스레드 전환이 발생하지 않더라도 두 개의 서로 다른 캐시 된 카운터 사본이 두 개의 다른 CPU 코어에 존재하고 스레드가 각각 별도의 코어에서 실행 되었기 때문에 이런 일이 발생할 수 있습니다. 그 문제에 대해 하나의 스레드는 하나의 값으로 카운터를 가질 수 있고 다른 스레드는 캐싱으로 인해 완전히 다른 값으로 카운터를 가질 수 있습니다.

이 예제에서 중요한 것은 변수 카운터 가 메인 메모리에서 캐시로 읽히고, 캐시에서 업데이트되고, 나중에 메모리 장벽이 발생하거나 캐시 메모리가 다른 것에 필요할 때 불확실한 시점에 메인 메모리로 다시 쓰여지는 것입니다. volatile최대 및 할당 테스트는 원자 적이 지 않은 read+increment+write기계 명령어 세트 인 증분을 포함하여 이산 연산이므로 카운터를 만드는 것은 이 코드의 스레드 안전에 충분하지 않습니다 .

MOV EAX,counter
INC EAX
MOV counter,EAX

휘발성 변수는 완전히 형성된 객체에 대한 참조 만 읽거나 쓰는 (예를 들어 일반적으로 단일 지점에서만 작성되는) 예제와 같이 모든 작업이 "원자"인 경우에만 유용 합니다. 또 다른 예로는 쓰기시 복사 목록을 지원하는 휘발성 배열 참조가 있습니다. 단, 배열에 대한 참조의 로컬 사본을 먼저 가져 와서 만 배열을 읽은 경우입니다.


5
매우 감사합니다! 카운터가있는 예는 이해하기 쉽습니다. 그러나 상황이 현실이되면 조금 다릅니다.
Albus Dumbledore

"실제로, 현재 하드웨어에서는 일반적으로 모니터를 확보 할 때 CPU 캐시가 플러시되고 해제 될 때 주 메모리에 쓰는데 둘 다 비싸다 (상대적으로 말하면)." . CPU 캐시를 말할 때 각 스레드에 대한 Java 스택과 동일합니까? 또는 스레드에 자체 로컬 버전의 힙이 있습니까? 내가 바보 같은 경우 사과하십시오.
NishM

1
@nishm 동일하지는 않지만 관련된 스레드의 로컬 캐시를 포함합니다. .
Lawrence Dol

1
@ MarianPaździoch : 증가 또는 감소는 읽기 또는 쓰기 가 아니며 읽기 쓰기입니다. 레지스터에 대한 읽기, 레지스터 증가, 메모리에 대한 쓰기입니다. 읽기 및 쓰기는 개별적으로 원 자성이지만 이러한 여러 작업은 그렇지 않습니다.
Lawrence Dol

2
그래서, 질문에 따라, 하지 만든 작업 만 잠금 획득 이후는 잠금 해제 후 보이게되지만 모든 스레드에 의해 행동을 보이게된다. 잠금 획득 전에 수행 된 작업
Lii

97

volatile필드 수정 자 이며, 동기화 되면 코드 블록메소드가 수정 됩니다. 따라서이 두 키워드를 사용하여 간단한 접근 자의 세 가지 변형을 지정할 수 있습니다.

    int i1;
    int geti1() {return i1;}

    volatile int i2;
    int geti2() {return i2;}

    int i3;
    synchronized int geti3() {return i3;}

geti1()i1현재 스레드에 현재 저장된 값에 액세스합니다 . 스레드는 변수의 로컬 사본을 가질 수 있으며 데이터는 다른 스레드에 보유 된 데이터와 동일 할 필요는 없습니다. 특히, 다른 스레드가 스레드 i1에서 업데이트되었을 수 있지만 현재 스레드의 값은 해당 스레드와 다를 수 있습니다 업데이트 된 값. 사실 자바는 "메인"메모리라는 개념을 가지고 있으며 이것은 변수에 대한 현재 "올바른"값을 보유하는 메모리입니다. 스레드는 변수에 대한 자체 데이터 사본을 가질 수 있으며 스레드 사본은 "메인"메모리와 다를 수 있습니다. "메인"메모리의 값을 가질 이처럼 사실, 가능한 대를 i1thread1이 값이하기 위해서는 2를 위해 i1및 위해 thread2thread1thread2 가 i1을 갱신했지만 갱신 된 값이 아직 "메인"메모리 나 다른 스레드로 전파되지 않은 경우 값은 3 입니다 .i1

한편, "메인"메모리 geti2()의 값에 효과적으로 액세스합니다 i2. 휘발성 변수는 현재 "메인"메모리에있는 값과 다른 변수의 로컬 사본을 가질 수 없습니다. 실제로 volatile로 선언 된 변수는 모든 스레드에서 데이터를 동기화해야하므로 스레드에서 변수에 액세스하거나 변수를 업데이트 할 때마다 다른 모든 스레드가 즉시 동일한 값을 볼 수 있습니다. 일반적으로 휘발성 변수는 "일반"변수보다 액세스 및 업데이트 오버 헤드가 높습니다. 일반적으로 스레드는 더 나은 효율성을 위해 자체 데이터 사본을 가질 수 있습니다.

자발성과 동기화 사이에는 두 가지 차이점이 있습니다.

첫 번째 동기화는 모니터에서 잠금을 획득 및 해제하여 한 번에 하나의 스레드 만 코드 블록을 실행하도록 할 수 있습니다. 그것은 잘 알려진 측면입니다. 그러나 동기화는 메모리도 동기화합니다. 실제로 동기화는 스레드 메모리 전체를 "메인"메모리와 동기화합니다. 따라서 실행 geti3()은 다음을 수행합니다.

  1. 스레드는 this 객체에 대한 모니터의 잠금을 획득합니다.
  2. 스레드 메모리는 모든 변수를 플러시합니다. 즉, "main"메모리에서 모든 변수를 효과적으로 읽습니다.
  3. 코드 블록이 실행됩니다 (이 경우 반환 값을 i3의 현재 값으로 설정합니다.이 값은 "메인"메모리에서 방금 재설정되었을 수 있습니다).
  4. 변수에 대한 모든 변경 사항은 일반적으로 "메인"메모리에 기록되지만 geti3 ()의 경우에는 변경 사항이 없습니다.)
  5. 나사산이 모니터의 잠금 장치를 해제하여 물체를 고정시킵니다.

휘발성이 스레드 메모리와 "메인"메모리 사이에서 하나의 변수 값만 동기화하는 경우, 동기화는 스레드 메모리와 "메인"메모리 사이의 모든 변수 값을 동기화하고 모니터를 잠그고 해제하여 부팅합니다. 명확하게 동기화되면 휘발성보다 더 많은 오버 헤드가 발생합니다.

http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html


35
-1, 휘발성은 잠금을 획득하지 않으며, 기본 CPU 아키텍처를 사용하여 쓰기 후 모든 스레드에서 가시성을 확보합니다.
Michael Barker

쓰기의 원 자성을 보장하기 위해 잠금이 사용될 수있는 경우가 있다는 점은 주목할 가치가 있습니다. 예를 들어 확장 너비 권한을 지원하지 않는 32 비트 플랫폼에서 long을 작성하십시오. Intel은 SSE2 레지스터 (128 비트 폭)를 사용하여 변동성을 길게 처리함으로써이를 방지합니다. 그러나 잠금으로 휘발성을 고려하면 코드에 불쾌한 버그가 발생할 수 있습니다.
Michael Barker

2
잠금에 의해 공유되는 중요한 의미 론적 변수는 변수가 Happens-Before edge (Java 1.5 이상)를 제공한다는 것입니다. 동기화 된 블록에 들어가고, 잠금을 해제하고 휘발성 물질을 읽는 것은 모두 "획득"으로 간주되며 잠금 해제, 동기화 된 블록을 종료하고 휘발성 물질을 작성하는 것은 모두 "릴리스"의 형태입니다.
Michael Barker

20

synchronized메소드 레벨 / 블록 레벨 액세스 제한 수정 자입니다. 하나의 스레드가 중요한 섹션에 대한 잠금을 소유하는지 확인합니다. 잠금 장치를 소유 한 스레드 만 synchronized블록에 들어갈 수 있습니다 . 다른 스레드가이 중요 섹션에 액세스하려고하면 현재 소유자가 잠금을 해제 할 때까지 기다려야합니다.

volatile모든 스레드가 주 메모리에서 변수의 최신 값을 가져 오도록하는 변수 액세스 수정 자입니다. volatile변수 에 액세스하기 위해 잠금이 필요하지 않습니다 . 모든 스레드는 휘발성 변수 값에 동시에 액세스 할 수 있습니다.

휘발성 변수 : Datevariable 을 사용하는 좋은 예 입니다.

Date 변수를 만들었다 고 가정하십시오 volatile. 이 변수에 액세스하는 모든 스레드는 항상 주 메모리에서 최신 데이터를 가져 오므로 모든 스레드가 실제 (실제) 날짜 값을 표시합니다. 동일한 변수에 대해 다른 시간을 보여주는 다른 스레드가 필요하지 않습니다. 모든 스레드는 올바른 날짜 값을 표시해야합니다.

여기에 이미지 설명을 입력하십시오

개념 을 더 잘 이해하려면 이 기사 를 살펴보십시오 volatile.

로렌스 돌 클리어는 당신을 설명했다 read-write-update query.

다른 검색어에 대해

동기화를 통해 변수에 액세스하는 것보다 변수를 휘발성으로 선언하는 것이 언제 더 적합한가?

당신은 사용해야하는 volatile모든 스레드가 I 날짜 변수에 대해 설명 한 예와 같이 실시간으로 변수의 실제 값을 얻을해야한다고 생각합니다.

입력에 의존하는 변수에 휘발성을 사용하는 것이 좋습니다?

대답은 첫 번째 쿼리와 동일합니다.

이해를 돕기 위해이 기사 를 참조하십시오 .


따라서 읽기는 동시에 발생할 수 있으며 CPU는 주 메모리를 CPU 스레드 캐시에 캐시하지 않으므로 쓰기는 어떻습니까? 동시 쓰기가 아니어야합니까? 두 번째 질문 : 블록이 동기화되었지만 변수가 휘발성이 아닌 경우 동기화 된 블록의 변수 값은 다른 코드 블록의 다른 스레드에 의해 여전히 변경 될 수 있습니까?
the_prole

11

tl; dr :

멀티 스레딩에는 3 가지 주요 문제가 있습니다.

1) 경쟁 조건

2) 캐싱 / 오래된 메모리

3) 컴파일러 및 CPU 최적화

volatile2 & 3을 해결할 수 있지만 1을 해결할 수 없습니다. synchronized/ explicit lock은 1, 2 & 3을 해결할 수 있습니다.

정교함 :

1)이 스레드가 안전하지 않은 코드를 고려하십시오.

x++;

하나의 작업처럼 보이지만 실제로는 3 : 메모리에서 x의 현재 값을 읽고 1을 더한 다음 다시 메모리에 저장합니다. 적은 수의 스레드가 동시에이를 시도하면 작업 결과가 정의되지 않습니다. x원래 1이었던 경우 코드를 작동하는 2 개의 스레드 후 제어가 다른 스레드로 전송되기 전에 작업의 어느 부분을 완료했는지에 따라 2 일 수 있으며 3 일 수 있습니다. 이것은 경쟁 조건 의 한 형태입니다 .

사용하여 synchronized코드 블록에하는 것은 만드는 원자 는 3 개 작업이 한 번에 발생하는 경우로 만들 것을 의미하고, 다른 스레드가 중간에 와서 방해 할 수있는 방법이 없다 -. 따라서 x1이고 2 개의 스레드 가 결국 x++우리가 알고 있는 프리폼 을 시도 하면 3과 같습니다. 따라서 경쟁 조건 문제를 해결합니다.

synchronized (this) {
   x++; // no problem now
}

표시 x등은 volatile하지 않는 x++;것이이 문제를 해결하지 않도록, 원자.

2) 또한 스레드에는 자체 컨텍스트가 있습니다. 즉, 메인 메모리에서 값을 캐시 할 수 있습니다. 즉, 일부 스레드에는 변수의 사본이있을 수 있지만 다른 스레드간에 변수의 새로운 상태를 공유하지 않고 작업 사본에서 작동합니다.

하나의 스레드에서을 고려하십시오 x = 10;. 그리고 나중에 다른 스레드에서 x = 20;. x다른 스레드가 새 값을 작업 메모리에 저장했지만 주 메모리에 복사하지 않았기 때문에 값 변경 이 첫 번째 스레드에 표시되지 않을 수 있습니다. 또는 메인 메모리에 복사했지만 첫 번째 스레드는 작업 복사본을 업데이트하지 않았습니다. 따라서 첫 번째 스레드를 확인 if (x == 20)하면 대답은입니다 false.

변수를 volatile기본으로 표시하면 모든 스레드가 주 메모리에서만 읽기 및 쓰기 작업을 수행하도록 지시합니다. synchronized모든 스레드가 블록에 들어갈 때 주 메모리에서 값을 업데이트하고 블록을 나갈 때 주 메모리로 결과를 플러시하도록 지시합니다.

데이터 레이스와 달리, 주 메모리로의 플러시가 발생하기 때문에 오래된 메모리는 (재) 생산하기가 쉽지 않습니다.

3) 컴파일러와 CPU는 (스레드 간 동기화 형식없이) 모든 코드를 단일 스레드로 처리 할 수 ​​있습니다. 의미하는 것은 멀티 스레딩 측면에서 매우 의미있는 일부 코드를 볼 수 있고 의미가없는 단일 스레드 인 것처럼 처리합니다. 따라서 코드를보고 최적화를 위해 코드를 다시 정렬하거나이 코드가 여러 스레드에서 작동하도록 설계되어 있는지 모르는 경우 코드의 일부를 완전히 제거할지 결정할 수 있습니다.

다음 코드를 고려하십시오.

boolean b = false;
int x = 10;

void threadA() {
    x = 20;
    b = true;
}

void threadB() {
    if (b) {
        System.out.println(x);
    }
}

20 으로 설정된 후에 만 ​​true로 설정된 threadB는 20 만 인쇄 할 수 있고 (또는 threadB if-check가 btrue로 설정되기 전에는 아무 것도 인쇄하지 않을 수 있음) 생각할 수 있지만 컴파일러 / CPU는 재정렬하기로 결정할 수 있습니다. 이 경우 threadA도 10을 인쇄 할 수 있습니다. 표시 는 순서가 변경되지 않거나 특정 경우에는 폐기되지 않습니다. 이는 threadB가 20 만 인쇄하거나 전혀 인쇄 할 수 없음을 의미합니다. 메소드를 동기화 된 것으로 표시하면 동일한 결과를 얻을 수 있습니다. 또한 변수를 표시 하면 순서가 다시 정렬되지 않지만 이전 / 이후의 모든 항목을 다시 정렬 할 수 있으므로 일부 시나리오에서는 동기화가 더 적합 할 수 있습니다.bxbvolatilevolatile

Java 5 New Memory Model 이전에는 휘발성이이 문제를 해결하지 못했습니다.


1
"한 번의 작업처럼 보이지만 실제로는 메모리에서 x의 현재 값을 읽고 1을 더한 다음 다시 메모리에 저장합니다." -맞습니다. 메모리의 값이 추가 / 수정 되려면 CPU 회로를 거쳐야하기 때문입니다. 이것이 단일 어셈블리 INC작업 으로 바뀌더라도 기본 CPU 작업은 여전히 ​​3 배이며 스레드 안전을 위해 잠금이 필요합니다. 좋은 지적. 그러나 INC/DEC명령은 어셈블리에서 원자 적으로 플래그를 지정할 수 있으며 여전히 1 개의 원자 작업입니다.
좀비

@Zombies 그래서 x ++에 대한 동기화 된 블록을 만들 때 플래그가 지정된 원자 INC / DEC로 바뀌거나 일반 잠금을 사용합니까?
David Refaeli

몰라요! 내가 아는 것은 INC / DEC가 원자가 아니라는 것입니다 .CPU의 경우 다른 산술 연산과 마찬가지로 값을로드하고 읽은 다음 메모리에 기록해야하기 때문입니다.
좀비
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.