자바 메모리 모델과 volatile 키워드
자바 메모리 모델과 volatile 키워드
volatile 키워드는 자바 가상 머신이 제공하는 가장 가벼운 동기화 메커니즘이지만, 저를 포함한 많은 개발자들이 synchronized 를 주로 활용하고는 합니다. 이번 게시글에서는 volatile 키워드에 대한 이해도를 높일 수 있도록 내용을 정리해보고자 합니다.
volatile 키워드는 2개의 특성을 부여합니다.
- 1번. 모든 스레드에서 이 변수를 투명하게 볼 수 있도록 가시성을 보장합니다. 한 스레드가 값을 수정하면 다른 스레드도 새로운 값을 즉시 알 수 있게 됩니다.(락을 보장한다는 의미는 아닙니다)
- 2번. 명령어 재정렬 최적화를 막아준다는 것입니다.
가시성을 보장
1번의 의미는 다음과 같은 시나리오를 통해서 확인해 볼 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
volatile boolean shupdownRequested;
public void shutdown() {
shupdownRequested = true;
}
public void doSomething() {
while (!shupdownRequested) {
// do Something...
}
}
shutDown 메서드가 실행되는 즉시, 모든 스레드에서 실행중인 doSomething 메서드가 종료될 것입니다.
그러나 락을 보장하지는 않고 가시성만을 보장한다는 의미는 아래의 코드로 확인할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
static volatile int race = 0;
/**
* 20 개의 스레드에서 동시에 아래 메서드를 실행
*/
public static void addRace() {
race++;
}
race 의 값은 20이 될까요? 아닙니다. 각 쓰레드에서 race++ 을 수행하기 위해서 읽어오는 시점의 race 값과, 저장하는 시점에(다른 스레드가 업데이트한) race 값이 다르므로 20이 아닐 것입니다. 따라서 가시성을 보장하지만 동시성 제약을 제공하지 않음을 알 수 있습니다.
재정렬 최적화를 방지
재정렬 최적화를 방지한다는 말이 어려울 수 있지만, 이는 변수 할당 작업의 순서를 보장한다는 말입니다. 이는 어셈블리 코드를 확인해봐야 알 수 있는데, 컴파일된 코드는 작업의 순서가 개발자가 작성한 의사 코드의 순서와 다를 수 있습니다.
예를 들어 아래와 같은 프로그램이 있습니다.
- 주소 A 의 값에 10 을 더한다.
- 주소 A 의 값에 2 를 곱한다.
- 주소 B 의 값에 3을 뺀다.
명령어 2는 명령어 1에 의존하기 때문에 둘의 순서는 재정렬 할 수 없지만, 3은 1과 2의 앞이나 중간 어디에든 재정렬될 수 있습니다. 프로그램의 실행 결과에 영향을 미치지 않기 때문입니다. 그러나 volatile 키워드를 사용하면, 일종의 장벽을 세워서 동시 실행 환경에서 안정성을 보장해줍니다.
volatile 키워드의 장단점
volatile 은 특정 상황에서 락 방식(synchronized 키워드)보다 성능이 좋으며, 읽기 성능인 일반 변수와 거의 동일합니다. 그러나 쓰기 상황에서는 메모리 장벽을 세우기 때문에 더 느릴 수 있습니다.
여러 상황에 맞춰서 가시성을 보장해야 하는 경우와, 동시 쓰기의 순서를 보장해야 하는 경우 등 volatile 키워드를 락 대신에 사용할 가치는 충분해보입니다.
원자성, 가시성, 실행 순서
- 원자성이 필요하다면 synchronized 키워드를 사용해야 합니다.
- 가시성이 필요하다면 volatile, synchronized, final 키워드를 사용해야 합니다.
- 실행 순서의 보장이 필요하다면 volatile, synchronized 키워드를 사용해야 합니다.