개발

java volatile

kwony 2026. 1. 8. 15:21

아래와 같은 클래스가 있다고 해보자. 

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 는 알려줬다. 

 

개념 자체는 알겠는데 실전에서 쓰려면 기억이 안나서 실수가 잦는 키워드다.