원자 / 휘발성 / 동기화의 차이점은 무엇입니까?


297

원자 / 휘발성 / 동기화는 내부적으로 어떻게 작동합니까?

다음 코드 블록의 차이점은 무엇입니까?

코드 1

private int counter;

public int getNextUniqueIndex() {
    return counter++; 
}

코드 2

private AtomicInteger counter;

public int getNextUniqueIndex() {
    return counter.getAndIncrement();
}

코드 3

private volatile int counter;

public int getNextUniqueIndex() {
    return counter++; 
}

volatile다음과 같은 방식으로 작동 합니까 ? 입니다

volatile int i = 0;
void incIBy5() {
    i += 5;
}

에 해당

Integer i = 5;
void incIBy5() {
    int temp;
    synchronized(i) { temp = i }
    synchronized(i) { i = temp + 5 }
}

두 개의 스레드가 동시에 동기화 된 블록에 들어갈 수 없다고 생각합니다 ... 맞습니까? 이것이 사실이라면 어떻게 atomic.incrementAndGet()작동 synchronized합니까? 그리고 스레드 안전합니까?

휘발성 변수 / 원자 변수에 대한 내부 읽기와 쓰기의 차이점은 무엇입니까? 스레드가 변수의 로컬 사본을 가지고 있다는 기사를 읽었습니다. 무엇입니까?


5
컴파일하지 않는 코드로 많은 질문을합니다. Java Concurrency in Practice와 같은 좋은 책을 읽어보십시오.
JB 니 제트

4
@JBNizet 당신이 맞아요 !!! 나는 그 책을 가지고 있는데, 그것은 원자 개념을 간략하게 가지고 있지 않으며 그 개념을 얻지 못한다. 저주의 저의 실수는 저자가 아닙니다.
hardik

4
실제로 어떻게 구현되는지 신경 쓰지 않아도됩니다 (OS에 따라 다릅니다). 이해해야 할 것은 계약입니다. 값은 원자 적으로 증가하며 다른 모든 스레드는 새로운 값을 볼 수 있습니다.
JB 니 제트

답변:


392

내부적으로 어떻게 작동 하는지에 대해 구체적으로 묻는 것이므로 다음과 같습니다.

동기화 안함

private int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

기본적으로 메모리에서 값을 읽고 증가시키고 메모리로 되돌립니다. 이것은 단일 스레드에서 작동하지만 현재 멀티 코어, 멀티 CPU, 멀티 레벨 캐시 시대에는 올바르게 작동하지 않습니다. 우선 경쟁 조건 (여러 스레드가 동시에 값을 읽을 수 있음)뿐만 아니라 가시성 문제가 발생합니다. 이 값은 " 로컬 "CPU 메모리 (일부 캐시) 에만 저장 될 수 있으며 다른 CPU / 코어 (및 스레드)에는 표시되지 않습니다. 이것이 많은 사람들이 스레드에서 변수의 로컬 사본 을 참조하는 이유 입니다. 매우 안전하지 않습니다. 이 인기 있지만 깨진 스레드 중지 코드를 고려하십시오.

private boolean stopped;

public void run() {
    while(!stopped) {
        //do some work
    }
}

public void pleaseStop() {
    stopped = true;
}

변수에 추가 volatile하면 stopped잘 작동합니다. 다른 스레드가 메소드 stopped를 통해 변수를 수정 pleaseStop()하면 작업 스레드 while(!stopped)루프 에서 즉시 변경 사항을 볼 수 있습니다 . BTW 이것은 스레드를 중단시키는 좋은 방법이 아닙니다. 사용하지 않고 영원히 실행되는 스레드를 중지하는 방법특정 Java 스레드 중지를 참조하십시오 .

AtomicInteger

private AtomicInteger counter = new AtomicInteger();

public int getNextUniqueIndex() {
  return counter.getAndIncrement();
}

AtomicInteger클래스는 CAS ( 비교 및 스왑 ) 저수준 CPU 작업 (동기화 필요 없음)을 사용합니다. 현재 값이 다른 값과 같고 성공적으로 반환 된 경우에만 특정 변수를 수정할 수 있습니다. 따라서 실행할 getAndIncrement()때 실제로 루프에서 실행됩니다 (간단한 실제 구현).

int current;
do {
  current = get();
} while(!compareAndSet(current, current + 1));

그래서 기본적으로 : 읽기; 증가 된 값을 저장하십시오. 성공하지 못한 경우 (값이 더 이상 같지 않음 current) 읽고 다시 시도하십시오. 은 compareAndSet()네이티브 코드 (조립)에서 구현됩니다.

volatile 동기화하지 않고

private volatile int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

이 코드는 올바르지 않습니다. 가시성 문제를 해결하지만 ( volatile다른 스레드가에 대한 변경 사항을 볼 수 있도록 counter) 여전히 경쟁 조건이 있습니다. 이것은 여러 번 설명 되었습니다 : 사전 / 사후 증가는 원자 적이 지 않습니다.

유일한 부작용 volatile은 캐시를 " 플러시 "하여 다른 모든 당사자가 최신 버전의 데이터를 볼 수 있도록하는 것입니다. 이것은 대부분의 상황에서 너무 엄격합니다. 이것이 volatile기본이 아닌 이유 입니다.

volatile 동기화하지 않고 (2)

volatile int i = 0;
void incIBy5() {
  i += 5;
}

같은 위와 문제 때문이 아니라 더 악화 i되지 않습니다 private. 경쟁 조건이 여전히 존재합니다. 왜 문제입니까? 예를 들어, 두 개의 스레드가이 코드를 동시에 실행하면 출력은 + 5또는 일 수 있습니다 + 10. 그러나 변경 내용이 표시됩니다.

여러 독립 synchronized

void incIBy5() {
  int temp;
  synchronized(i) { temp = i }
  synchronized(i) { i = temp + 5 }
}

놀랍게도이 코드도 올바르지 않습니다. 사실, 그것은 완전히 잘못되었습니다. 우선, 당신이 동기화 i하려고하고 있습니다. 변경 될 예정입니다 (더욱 i원시적이므로 Integer오토 박스를 통해 생성 된 임시에서 동기화하고 있다고 생각합니다 ...) 완전히 결함이 있습니다. 당신은 또한 쓸 수 있습니다 :

synchronized(new Object()) {
  //thread-safe, SRSLy?
}

두 개의 스레드가 동일한 잠금으로 동일한 synchronized블록에 들어갈 수 없습니다 . 이 경우 (및 마찬가지로 코드에서) 잠금 객체는 모든 실행마다 변경되므로 효과적으로 영향을 미치지 않습니다.synchronized

this동기화에 최종 변수 (또는 )를 사용한 경우에도 코드는 여전히 올바르지 않습니다. 두 개의 스레드가 먼저 읽을 i수 있습니다.temp 동 기적으로 (로컬에서 같은 값을 갖는 temp으로, 다음 첫 번째 할당하는 새로운 값) i(1 ~ 6 말을) 다른 하나는 (1 ~ 6) 같은 일을한다.

읽기에서 값 할당까지 동기화가 이루어져야합니다. 첫 번째 동기화는 영향을 미치지 않으며 (읽는 int것은 원자적임) 두 번째 동기화는 영향을 미치지 않습니다 . 제 생각에는 올바른 형식입니다.

void synchronized incIBy5() {
  i += 5 
}

void incIBy5() {
  synchronized(this) {
    i += 5 
  }
}

void incIBy5() {
  synchronized(this) {
    int temp = i;
    i = temp + 5;
  }
}

10
내가 추가 할 유일한 것은 JVM이 변수 값을 레지스터에 복사하여 변수 값을 조작한다는 것입니다. 이는 단일 CPU / 코어에서 실행되는 스레드가 여전히 비 휘발성 변수에 대해 다른 값을 볼 수 있음을 의미합니다.
David Harkness

@ Thomasz : compareAndSet (현재, 현재 + 1) 동기화입니까 ?? 두 스레드가 동시에이 메소드를 실행할 때 발생하는 일보다 더 적은 경우 ??
hardik

@Hardik : compareAndSetCAS 작업에 대한 간단한 래퍼입니다. 나는 대답에서 몇 가지 세부 사항을 봅니다.
Tomasz Nurkiewicz

1
@thomsasz : 좋아, 나는이 링크 질문을 통해 jon skeet가 대답했다. "스레드는 다른 스레드가 쓰기를 수행했는지 여부를 확인하지 않고 휘발성 변수를 읽을 수 없습니다." 그러나 하나의 스레드가 쓰기 작업과 두 번째 스레드 사이에있는 경우 어떻게됩니까! 내가 잘못 ?? 원자 작동시 경쟁 조건이 아닙니까 ??
hardik

3
@Hardik : 질문에 대한 답변을 더 얻으려면 다른 질문을 작성하십시오. 여기는 본인과 본인이며 의견은 질문에 적합하지 않습니다. 새 질문에 대한 링크를 여기에 게시하여 후속 조치를 취할 수 있도록하십시오.
Tomasz Nurkiewicz

61

변수를 휘발성 으로 선언하면 해당 값을 수정하면 변수의 실제 메모리 스토리지에 즉시 영향을 미칩니다. 컴파일러는 변수에 대한 참조를 최적화 할 수 없습니다. 이것은 하나의 스레드가 변수를 수정할 때 다른 모든 스레드가 즉시 새 값을 보게합니다. (이는 비 휘발성 변수에 대해서는 보장되지 않습니다.)

원자 변수를 선언하면 변수에 대한 작업이 원자 방식으로 발생합니다. 즉, 작업의 모든 하위 단계가 실행되는 스레드 내에서 완료되고 다른 스레드에 의해 중단되지 않습니다. 예를 들어, 증가 및 테스트 작업에서는 변수를 증가시킨 다음 다른 값과 비교해야합니다. 원 자성 조작은이 두 단계가 마치 하나의 분리 불가능 / 무정전 조작 인 것처럼 완료되도록 보장합니다.

변수에 대한 모든 액세스를 동기화 하면 한 번에 하나의 스레드 만 변수에 액세스 할 수 있으며 다른 모든 스레드는 해당 액세스 스레드가 변수에 대한 액세스를 해제 할 때까지 대기합니다.

동기화 된 액세스는 원자 적 액세스와 유사하지만 원 자성 작업은 일반적으로 낮은 수준의 프로그래밍에서 구현됩니다. 또한 변수에 대한 일부 액세스 만 동기화하고 다른 액세스는 비 동기화 할 수 있습니다 (예 : 모든 쓰기를 변수에 동기화하지만 변수에 대한 읽기는 동기화하지 않음).

원 자성, 동기화 및 변동성은 독립적 인 속성이지만 일반적으로 변수에 액세스하기 위해 적절한 스레드 협력을 시행하기 위해 조합하여 사용됩니다.

부록 (2016 년 4 월)

변수에 대한 동기화 된 액세스는 일반적으로 모니터 또는 세마포어를 사용하여 구현됩니다 . 이것들은 스레드가 변수 나 코드 블록에 대한 제어권을 독점적으로 획득 할 수 있도록하는 저수준 뮤텍스 (mutual exclusion) 메커니즘으로, 다른 모든 스레드가 동일한 뮤텍스를 획득하려고 시도 할 때까지 기다리도록합니다. 소유 스레드가 뮤텍스를 해제하면 다른 스레드가 차례로 뮤텍스를 획득 할 수 있습니다.

부록 (2016 년 7 월)

동기화는 개체에서 발생 합니다 . 이것은 클래스의 동기화 된 메소드를 호출하면 호출의 this객체를 잠급니다 . 정적 동기화 메소드는 Class객체 자체를 잠급니다 .

마찬가지로, 동기화 된 블록을 입력하려면 this메소드 의 오브젝트를 잠 가야합니다 .

즉, 동기화 된 메소드 (또는 블록)가 다른 객체에서 잠금을 실행하는 경우 동시에 여러 스레드에서 여러 스레드에서 실행될 수 있지만 주어진 단일 객체에 대해 한 스레드에서만 한 번에 동기화 된 메소드 (또는 블록)를 실행할 수 있습니다 .


25

휘발성 물질:

volatile키워드입니다. volatile모든 스레드가 캐시 대신 기본 메모리에서 변수의 최신 값을 얻도록합니다. 휘발성 변수에 액세스하기 위해 잠금이 필요하지 않습니다. 모든 스레드는 휘발성 변수 값에 동시에 액세스 할 수 있습니다.

volatile변수를 사용하면 휘발성 변수에 대한 쓰기가 동일한 변수의 후속 읽기와의 관계를 확립하기 때문에 메모리 일관성 오류의 위험이 줄어 듭니다.

이것은 volatile변수 에 대한 변경 사항 이 항상 다른 스레드에 표시 된다는 것을 의미합니다 . 또한 스레드가 volatile변수를 읽을 때 휘발성에 대한 최신 변경뿐만 아니라 변경을 초래 한 코드의 부작용도 볼 수 있습니다 .

사용시기 : 하나의 스레드가 데이터를 수정하고 다른 스레드는 최신 데이터 값을 읽어야합니다. 다른 스레드는 조치를 취하지 만 data를 업데이트하지는 않습니다 .

AtomicXXX :

AtomicXXX클래스는 단일 변수에서 잠금없는 스레드 안전 프로그래밍을 지원합니다. 이러한 AtomicXXX클래스 (같은 AtomicInteger)는 여러 스레드에서 액세스 된 휘발성 변수 수정의 메모리 불일치 오류 / 부작용을 해결합니다.

사용시기 : 여러 스레드가 데이터를 읽고 수정할 수 있습니다.

동기화 :

synchronized메소드 또는 코드 블록을 보호하는 데 사용되는 키워드입니다. 동기화 된 방법으로 두 가지 효과가 있습니다.

  1. 첫째, synchronized동일한 객체에서 두 개의 메소드 호출이 인터리브 될 수 없습니다. 하나의 스레드가 synchronized객체에 대한 메소드를 실행하는 synchronized경우 첫 번째 스레드가 객체와 함께 완료 될 때까지 동일한 객체 블록에 대한 메소드를 호출 하는 다른 모든 스레드 (실행 중단).

  2. 둘째, synchronized메소드가 종료되면 synchronized메소드는 동일한 오브젝트 에 대한 메소드 의 후속 호출과의 사전 관계를 자동으로 설정 합니다. 이를 통해 객체 상태의 변경 사항이 모든 스레드에 표시됩니다.

사용시기 : 여러 스레드가 데이터를 읽고 수정할 수 있습니다. 비즈니스 로직은 데이터를 업데이트 할뿐만 아니라 원 자성 작업을 실행합니다

AtomicXXX의 것과 동일 volatile + synchronized구현이 다른 경우에도. 변수 + 메소드를 AmtomicXXX확장 하지만 동기화를 사용하지 않습니다.volatilecompareAndSet

관련 SE 질문 :

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

휘발성 부울 대 원자 부울

읽어야 할 좋은 기사 : (위의 내용은이 문서 페이지에서 가져 왔습니다)

https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html

https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html


2
이것은 실제로 설명 된 키워드 / 기능의 발생 시맨틱을 언급하는 첫 번째 답변으로, 실제로 코드 실행에 미치는 영향을 이해하는 데 중요합니다. 높은 투표 응답은이 측면을 놓칩니다.
jhyot

5

두 개의 스레드가 동시에 동기화 블록에 들어갈 수 없다는 것을 알고 있습니다.

두 스레드는 동일한 객체에서 동기화 된 블록을 두 번 입력 할 수 없습니다. 이것은 두 개의 스레드가 다른 객체에서 동일한 블록에 들어갈 수 있음을 의미합니다. 이 혼란은 이와 같은 코드로 이어질 수 있습니다.

private Integer i = 0;

synchronized(i) {
   i++;
}

매번 다른 객체를 잠글 수 있으므로 예상대로 작동하지 않습니다.

이것이 atomic.incrementAndGet ()이 동기화없이 작동하는 방법보다 사실이라면 ?? 스레드 안전합니까 ??

예. 스레드 안전을 달성하기 위해 잠금을 사용하지 않습니다.

그들이 어떻게 작동하는지 더 자세히 알고 싶다면 그것들을위한 코드를 읽을 수 있습니다.

그리고 휘발성 변수 / 원자 변수에 대한 내부 읽기와 쓰기의 차이점은 무엇입니까 ??

원자 클래스는 휘발성 필드를 사용 합니다. 현장에는 차이가 없습니다. 차이점은 수행 된 작업입니다. 원자 클래스는 CompareAndSwap 또는 CAS 작업을 사용합니다.

스레드가 변수의 로컬 사본을 가지고 있다는 기사를 읽었습니다.

각 CPU마다 다른 CPU와 다를 수있는 캐시 된 메모리 뷰가 있다는 사실을 언급한다고 가정 할 수 있습니다. CPU가 일관된 데이터보기를 갖도록하려면 스레드 안전 기술을 사용해야합니다.

이것은 적어도 하나의 스레드가 메모리를 공유 할 때만 문제가됩니다.


@Aniket Thakur 확실합니까? 정수는 변경할 수 없습니다. 따라서 i ++은 int 값을 자동으로 개봉하고 증가시킨 다음 이전과 동일한 인스턴스가 아닌 새로운 정수를 만듭니다. i를 최종적으로 시도하면 i ++를 호출 할 때 컴파일러 오류가 발생합니다.
fuemf5

2

동기화 된 대 원자 대 휘발성 :

  • 휘발성 및 원자는 variable에만 적용되고 Synchronized는 메소드에 적용됩니다.
  • 휘발성은 물체의 원 자성 / 일관성이 아닌 가시성을 보장하는 반면, 다른 것들은 가시성과 원 자성을 보장합니다.
  • RAM에 휘발성 변수 저장소가 있으며 액세스 속도가 빠르지 만 스레드 안전 또는 동기화 whitout 동기화 키워드를 얻을 수는 없습니다.
  • 동기화되지 않은 상태에서 동기화 된 블록 또는 동기화 된 방법으로 구현 된 동기화. 동기화 된 키워드를 사용하여 안전한 여러 줄의 코드를 스레드 할 수 있지만 둘 다 동일하지는 않습니다.
  • 동기화는 동일한 클래스 객체 또는 다른 클래스 객체를 잠글 수 있지만 둘 다 할 수는 없습니다.

내가 놓친 것이 있으면 저를 정정하십시오.


1

휘발성 + 동기화는 CPU에 대한 여러 명령을 포함하는 작업 (문)이 완전히 원자화되는 확실한 솔루션입니다.

예를 들어 : volatile int i = 2; i ++, i = i + 1에 지나지 않습니다. 이 문을 실행 한 후 i를 메모리의 값 3으로 만듭니다. 여기에는 i의 메모리에서 기존 값을 읽고 (2), CPU 누산기 레지스터에로드하고 기존 값을 1 (누적 기에서 2 + 1 = 3) 씩 증가시켜 계산 한 다음 증가 된 값을 다시 기록하는 작업이 포함됩니다. 메모리로 돌아갑니다. 값이 i 인 경우에도 이러한 연산은 원 자성이 충분하지 않습니다. 휘발성은 메모리에서 단일 읽기 / 쓰기가 원자 적이며 MULTIPLE이 아닌 원자 성임을 보장합니다. 따라서, 우리는 그것을 바보 증거 원자 성명으로 유지하기 위해 i ++ 주위에서도 동기화해야합니다. 명령문에 여러 명령문이 포함되어 있다는 사실을 기억하십시오.

설명이 충분히 명확하기를 바랍니다.


1

자바 휘발성 수정자는 스레드간에 통신이 이루어 지도록하는 특별한 메커니즘의 예입니다. 한 스레드가 휘발성 변수에 쓰고 다른 스레드가 해당 쓰기를 볼 때 첫 번째 스레드는 해당 휘발성 변수에 대한 쓰기를 수행 할 때까지 두 번째 메모리 내용을 알려줍니다.

원자 작업 은 다른 작업의 간섭없이 단일 작업 단위로 수행됩니다. 데이터 불일치를 피하기 위해 다중 스레드 환경에서 원자 작업이 필요합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.