java volatile
아래와 같은 클래스가 있다고 해보자.
public static class SharedClass {
private int x = 0;
private int y = 0;
public void increment() {
x++;
y++;
}
public void checkForDataRace() {
if (y > x) {
System.out.println("y > x - Data Race is detected");
}
}
}
x에 대한 증가가 y에 대한 증가보다 먼저 작성되었기 때문에 이론상으로 x>=y 인 상황만 발행해야하고 checkForDataRace 에서 if 문에 걸릴 일은 없다.
그런데 멀티 쓰레드 환경에서 실행해보면 y>x 인 케이스가 발생한다. 놀랍게도
public class Main {
public static void main(String[] args) {
SharedClass sharedClass = new SharedClass();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
sharedClass.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
sharedClass.checkForDataRace();
}
});
thread1.start();
thread2.start();
}
//
y > x - Data Race is detected
y > x - Data Race is detected
y > x - Data Race is detected
y > x - Data Race is detected
컴파일러와 CPU는 성능 최적화를 위해서 종종 연산의 순서를 바꾸기도 한다. 논리적인 정확성을 유지하면서 순서를 바꾸는데 멀티 쓰레드환경에서는 규칙이 지켜지지 않는 문제가 발생한다.
위의 예제 코드에선 y++ 를 CPU가 먼저 실행하고 thread2 에서 이때 상태로 check를 걸었기 때문에 문제가 된다.
해결책은 두가지가 있다.
첫번째는 SharedClass 의 함수에다가 synchronized 를 걸어주는 방법이다.
public static class SharedClass {
private int x = 0;
private int y = 0;
public synchronized void increment() {
x++;
y++;
}
public synchronized void checkForDataRace() {
if (y > x) {
System.out.println("y > x - Data Race is detected");
}
}
}
직관적이긴 하지만 이 방법은 Core Grained Locking 이 되어서 성능이 덜어진다.
두번째는 volatile 을 사용하는 방법이다. volatile 로 선언된 변수에 대해선 실행의 재정렬을 막아주고 변수의 변경을 쓰레드가 즉시 볼 수 있게 해준다.
위의 클래스에서 volatile 을 붙여주면 y>x 인 케이스가 발생하지 않게 된다
public static class SharedClass {
private volatile int x = 0;
private volatile int y = 0;
public void increment() {
x++;
y++;
}
public void checkForDataRace() {
if (y > x) {
System.out.println("y > x - Data Race is detected");
}
}
}
단 연산의 원자성을 보장하진 않는다. 멀티쓰레드 환경에서 여러 쓰레드가 동시에 연산을 실행하면 결과 값이 예상과 달라질 수 있다.
그래서 Volatile 의 경우 쓰레드 하나만 값을 업데이트하고 나머지는 읽기 연산을 수행할 때 쓰는게 좋다.
이걸 Single Writer Multiple Reader 패턴이라고 Chat GPT 는 알려줬다.
개념 자체는 알겠는데 실전에서 쓰려면 기억이 안나서 실수가 잦는 키워드다.