먼저 결론부터 이야기하자면,
- volatile 키워드를 지닌 변수는 반드시 메인 메모리에 쓰거나 읽게 된다.
- volatile 키워드를 지닌 변수는 최적화의 대상이 아니다.
1번 부터 설명을 해보자.
한 쓰레드가 공용 변수를 다룰때, 반드시 메인메모리에서 해당값을 읽어 오지는 않는다. CPU 코어에 있는 캐시 메모리에서 값을 가져오고 저장하게 된다.
volatile 키워드를 쓰게되면 해당 값을 저장할때는 반드시 메인 메모리에 저장하고, 읽을 때는 반드시 메인 메모리에 있는 값을 읽어 오게된다.
2번은 무슨뜻일까?
void test() {
i++;
j++;
}
이라는 코드가 존재할때, 우린 당연히 i 값이 먼저 증가하고 j 값이 증가하는 것을 기대한다.
하지만 실제로는 jvm 이 코드를 분석했을때, 결과에 영향을 미치지 않는다고 판단하면 순서를 변경할 수도 있다. volatile 키워드를 붙이게 되면 그러한 현상을 방지할 수 있다.
https://docs.oracle.com/javase/specs/jls/se17/html/jls-8.html#jls-8.3.1.4
그러면 oracle 공식문서 volatile 키워드 편에 있는 예시를 살펴보면서 volatile 의 사용처를 알아보자.
class Test {
static int i = 0, j = 0;
static void one() { i++; j++; }
static void two() {
System.out.println("i=" + i + " j=" + j);
}
}
위와 같은 클래스가 있다. 쓰레드 1이 one()을 반복적으로 실행하고, 쓰레드2가 two()를 반복적으로 실행하는 상황이다. 그럴때, 오라클은 two() 를 실행할때 j 값이 i 값 보다 큰 경우가 있을 수 있다고 이야기 한다. 아까 설명했던 컴파일러의 최적화 때문에 j++ 이 i++ 보다 먼저 실행될 수 있다는 것이다.
class Test {
static int i = 0, j = 0;
static synchronized void one() { i++; j++; }
static synchronized void two() {
System.out.println("i=" + i + " j=" + j);
}
}
그런상황을 타개하기 위해, synchronized 키워드를 도입했다. static 메소드에 synchronized 키워드가 붙었으므로, Test 클래스에 락이 걸리게 된다. 따라서 one() 과 two()는 동시에 실행될 수 없으므로, i값과 j값은 항상 같게 된다.
class Test {
static volatile int i = 0, j = 0;
static void one() { i++; j++; }
static void two() {
System.out.println("i=" + i + " j=" + j);
}
}
그렇다면 volatile 키워드를 쓰면 어떨까?
사실 이 경우에도 많은 문제들이 존재한다. 일단 i++ 이 j++ 보다 먼저 실행되는 것이 보장된다. 따라서, two() 가 실행되는 대부분의 경우에 i>=j 일 것이다. 하지만 오라클은 극히 드문 경우로 j 가 i 보다 큰 값이 출력될 수도 있다고 말한다. 메소드 two() 에서 i 를 읽어오고 j를 읽어오는 그 중간텀에 one() 이 실행된다면 j 값이 더 크게 출력될 수 도 있다는 뜻이다.
- volatile 키워드를 지닌 변수는 반드시 메인 메모리에 쓰거나 읽게 된다.
- volatile 키워드를 지닌 변수는 최적화의 대상이 아니다.
https://docs.oracle.com/javase/specs/jls/se17/html/jls-8.html#jls-8.3.1.4
https://docs.oracle.com/javase/specs/jls/se17/html/jls-17.html#jls-17.4-B