메서드 앞에 키워드를 붙여서 선언
public synchronized void foo() {}
특정 Thread가 블록 전체를 lock 하므로, 간단하면서 완벽한 thread-safe -> 다른 Thread는 아무런 작업을 하지 못하고 기다릴 수 밖에 없어 자원의 낭비 발생 가능
변수 앞에 키워드를 붙여서 선언
public volatile String str = "VOLATILE STRING";
Thread는 실행되고 있는 CPU 메모리 영역에 데이터를 캐싱함. 따라서 멀티 코어 프로세서에서 다수의 Thread가 변수 a를 공유하더라도 캐싱된 시점에 따라 데이터가 다를 수 있음(멀티 코어) -> 이말은 즉, 캐싱된 데이터가 언제 갱신되는지 또한 정확히 알 수 없다는 것
이런 경우 volatile 키워드를 사용하여 CPU 메모리 영역에 캐싱된 값이 아니라 항상 최신의 값을 가지도록 메인 메모리 영역에서 값을 참조하도록 할 수 있음 -> 즉, 동일 시점에 모든 스레드가 동일한 값을 가지도록 동기화함.
하지만 volatile을 통해 모든 동기화 문제가 해결되는 건 아님. (단지 모든 Thread가 캐시없이 최신의 값을 보게 할 뿐)
- 변수 a =1
- 1번 스레드가 a에 값을 증가시키기 위해 a의 값을 읽습니다. a=1
- 1번 스레드가 1 + 1을 계산하여 2를 얻습니다. (아직 저장은 안 함)
- 2번 스레드가 a에 값을 증가시키기 위해 a의 값을 읽습니다. a=1
- 1번 스레드가 변수 a에 2를 저장
- 2번 스레드가 1 + 1을 계산하여 2를 얻습니다. (아직 저장은 안 함)
- 2번 스레드가가 변수 a에 2를 저장
-> 최종 값이 2가 되는 문제
즉, volatile은 원자적 연산에서만 동기화를 보장한다.
위 문제들을 해결하기 위해, 비-원자적 연산에서도 동기화를 빠르고 쉽게 이용하기 위한 클래스 모음
java.util.concurrent.*
(대표적으로 컬렉션, Wrapper 클래스 등이 있음)
Non-Blocking 이 가능한 이유는 CAS(Compare-And-Swap)
알고리즘을 이용하기 때문
volatile 키워드를 이용하면서 현재 Thread에 저장된 값과 메인 메모리에 저장된 값을 비교함
- 일치하는 경우 새로운 값으로 교체(thread-safe한 상태이므로 로직 수행)
- 일치하지 않는 경우 실패 후 재시도(thread-safe하지 않은 상태였으므로 재시도)
// java.util.concurrent.atomic.AtomicLong;
public class AtomicLong extends Number implements java.io.Serializable {
private volatile long value;
public final long incrementAndGet() {
return U.getAndAddLong(this, VALUE, 1L) + 1L;
}
}
// 자바 네이티브 코드 부분 (jdk.internal.misc.Unsafe)
// weakCompareAndSetLong() : CAS 알고리즘
// AtomicLong.incrementAndGet() 메서드를 쭉 따라가면 JNI 코드로 이루어져 있음
// 메모리에 저장된 값과 CPU에 캐시된 값을 비교해 동일한 경우에만 update 수행
public final long getAndAddLong(Object o, long offset, long delta) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!weakCompareAndSetLong(o, offset, v, v + delta));
return v;
}