IT story

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

hot-time 2020. 4. 25. 09:56
반응형

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


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

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

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

...

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

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

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


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

  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

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


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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

다른 검색어에 대해

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

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

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

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

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


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 이전에는 휘발성이이 문제를 해결하지 못했습니다.


Jenkov의 설명이 마음에 듭니다 . 멀티 스레딩 환경.

공유 객체의 가시성

휘발성 선언이나 동기화를 올바르게 사용하지 않고 둘 이상의 스레드가 객체를 공유하는 경우 한 스레드에서 만든 공유 객체에 대한 업데이트가 다른 스레드에 표시되지 않을 수 있습니다.

공유 객체가 처음에 주 메모리에 저장되어 있다고 상상해보십시오. CPU 1에서 실행되는 스레드는 공유 객체를 CPU 캐시로 읽습니다. 공유 객체가 변경됩니다. CPU 캐시가 주 메모리로 다시 플러시되지 않는 한, 변경된 공유 객체 버전은 다른 CPU에서 실행중인 스레드에 표시되지 않습니다. 이러한 방식으로 각 스레드는 고유 한 공유 객체 사본으로 끝날 수 있으며 각 사본은 다른 CPU 캐시에 있습니다.

다음 다이어그램은 스케치 된 상황을 보여줍니다. 하나 개는 왼쪽 CPU 복사에 자사의 CPU 캐시에 공유 객체를 실행 스레드, 그리고 변화카운트 카운트 업데이트가 주요 플러시 다시되지 않았기 때문에, 2. 변수는이 변화가 바로 CPU에서 실행중인 다른 스레드되지 볼 수 있습니다 아직 메모리.

휘발성 [정보]

이 문제를 해결하기 위해 Java의 volatile 키워드를 사용할 수 있습니다 . 다음을 volatile keyword확인할 수 있습니다.

  1. 주어진 변수는 메인 메모리 에서 직접 읽고 업데이트 될 때 항상 메인 메모리에 다시 쓰여집니다 .
  2. 전에 일어난다.

경쟁 조건

둘 이상의 스레드가 객체를 공유하고 둘 이상의 스레드가 해당 공유 객체의 변수를 업데이트 race conditions하면 발생할 수 있습니다.

thread A공유 객체의 변수 개수를 CPU 캐시로 읽는 다고 상상해보십시오 . thread B똑같지 만 다른 CPU 캐시에 있다고 상상해보십시오 . 이제 thread A세는 것을 추가 thread B하고 똑같이합니다. 이제 각 CPU 캐시에서 var1이 두 번씩 증가했습니다 .

이러한 증분이 순차적 으로 수행 된 경우 변수 카운트가 두 번 증분되고 원래 값 + 2 가 주 메모리에 다시 기록됩니다.

그러나 두 동기화는 적절한 동기화없이 동시에 수행되었습니다. 업데이트 된 버전의 카운트를 주 메모리에 다시 쓰는 스레드 A와 B에 관계없이 업데이트 된 값은 두 증분에도 불구하고 원래 값보다 1 더 높습니다.

이 다이어그램은 위에서 설명한 경쟁 조건에서 발생하는 문제를 보여줍니다.

동기화 됨 [정보]

이 문제를 해결하기 위해 synchronized

  1. 동기화 된 블록은 한 번에 하나의 스레드 만 주어진 코드의 중요 섹션에 들어갈 수 있도록합니다 .
  2. 전에 일어난다.

관련 주제 비교 및 교체

참고 URL : https://stackoverflow.com/questions/3519664/difference-between-volatile-and-synchronized-in-java

반응형