해당 포스트는 Effective JAVA Item.78 중 Atomic
과 volatile
에 대해서 공부한 과정을 기록하기 위해 작성했다.
공유 중인 가변 데이터에 여러 개의 스레드가 동시에 접근하게 되면 Race Condition이 발생할 수도 있고, 가시성 문제가 발생할수 있다. 일반적으로 JAVA 에서는 동기화에 대한 문제를 synchronized
, volatile
, Atomic.class
를 사용해서 해결한다.
synchronized
는 선언된 메서드의 코드 섹션 전체에 락을 걸고 접근하는 스레드들은 block or suspended 상태로 변경되게 된다. 스레드들이 blocking 되는 과정과 다시 resuming 되는 과정에서 시스템의 자원을 소모하게 된다. 100개의 스레드가 동시에 접근을 한다면, 99개의 스레드가 이러한 과정을 거치게 되는 것이다. 바로 이 부분에서 성능 저하가 발생한다.
Atomic
의 핵심은 이러한 소모 비용을 줄이는 non-blocking
방식을 사용한다는 점에서 차이점이 존재한다. 즉, 어떤 스레드도 suspended 되지 않기 때문에 context switch를 피할 수 있다.
Atmoic
의 동작 과정은 다음과 같은 방식의 CAS 알고리즘
을 사용한다.
기존 값과 현재 메모리가 가지고 있는 값이 다른 경우는 어떤 경우일까? 아래 그림을 보자.
스레드는 메모리의 데이터를 가져다가 캐시에 올려놓고 연산을 하게 된다. 연산을 마치면 연산 결과를 다시 메모리에 반영시키는 것이다. 만약 연산을 수행하는 스레드가 여러 개라면 메모리의 데이터와 스레드의 데이터가 불일치하는 상황이 발생하는데 이러한 경우 false
를 반환한다는 것이다.
AtomicInteger
의 incrementAndGet
메서드를 보면 내부적으로 CAS 알고리즘
을 구현하고 있는 것을 알 수 있다.
public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe U = Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
private volatile int value;
public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1;
}
}
public final class Unsafe {
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
}
AtomicInteger는 weakCompareAndSetInt
의 return 값이 true 로 반환할 될 때까지 무한 루프를 돌면서 기다린다. 그러면 여기서 의문이 하나 생긴다. 의미없이 무한 루프를 돌면서 CPU를 붙잡고 있는데 synchronized
보다 나을까 하는 의문이다.
앞서 언급했듯 synchronized
는 스레드가 suspending
과 resuming
하는데에 자원 소모가 발생하고, 이 과정은 lock이 걸린 모든 스레드가 공통적으로 겪는 과정이다. 그렇기 때문에 비록 무한 루프를 돌지만 true를 return 받는 즉시 다음 작업을 수행할 수 있는 Atomic
이 성능적으로 우수하다.
volatile
이라는 키워드다.volatile?
volatile
은 배타적 수행과는 상관없지만 항상 최근에 기록된 값을 읽는 것을 보장해준다. 메인 메모리에 있는 값을 직접 읽어서, 캐시 메모리를 사용하는 구조상 발생할 수 있는 가시성의 문제를 해결해줄 수 있다.
class AtomicInteger {
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
public AtomicInteger() {
}
public final int get() {
return value;
}
public final void set(int newValue) {
value = newValue;
}
...
}
get()과 set()은 그 자체로 atomic 연산이기 때문에 race condition이 발생하지 않는다. 따라서 volatile
한정자를 사용해서 가시성에 대한 문제만을 해결해준것이다.