[Java] Volatile ? 휘발성 객체 ?

HenryHong·2026년 1월 8일

java

목록 보기
10/15
post-thumbnail

자바 volatile = 멀티스레드를 공부하다보면 마주치는 생소한 키워드 😅


1. 왜 굳이 volatile 을 써야 할까?

멀티 스레드 코드를 짜다 보면, 가끔 이런 기분 나쁜 일이 생긴다.

  • 분명 한 스레드에서 값을 바꿨는데,
  • 다른 스레드는 그 값을 한참 뒤에 보거나, 심하면 영원히 못 보기도 한다.

이유는 단순하다.

  • CPU는 성능을 위해 값을 메인 메모리 → CPU 캐시/레지스터로 가져와서 거기서 작업한다.
  • JVM/JIT도 “이 변수 어차피 안 바뀌겠지?”라고 가정하고 캐시에 들고 다니거나, 명령 순서를 바꾼다.

그 결과, 같은 변수를 공유하는데도 스레드마다 서로 다른 값을 보고 있는 상황이 생긴다.
이게 바로 메모리 가시성(visibility) 문제다.


2.volatile 이 뭔데?

volatile을 한 줄로 요약하면 이렇게 말할 수 있다.

“이 변수는 여러 스레드가 같이 보니까,
항상 최신 값을 메인 메모리 기준으로 보게 하고, 앞뒤 순서도 꼬지 마

조금 더 공식적으로 말하면, volatile은 두 가지를 보장한다.

  1. 가시성(Visibility)

    • 한 스레드가 volatile 변수에 쓴 값은 즉시 메인 메모리에 반영된다.
    • 다른 스레드는 이 변수를 읽을 때 항상 최신 값을 보게 된다.
  2. 명령 재정렬 방지(Happens-before)

    • 어떤 스레드 A의 volatile write 이전에 일어난 일들은,
    • 다른 스레드 B가 같은 변수를 volatile read 한 이후에는 이미 일어난 것으로 보장된다.

즉, 단순히 “캐시 쓰지 마” 수준이 아니라,
해당 변수를 기준으로 메모리 배리어를 세우는 동기화 도구라고 보면 된다.


3. 고전 예제로 감 잡기

먼저 volatile 없는 버전부터 보자.

public class Runner {
    private static int number = 0;
    private static boolean ready = false;

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!ready) {
                // busy-wait
            }
            System.out.println(number);
        });
        t.start();

        number = 42;
        ready = true;
    }
}

개발자 감각으로는 “무조건 42 찍히겠지?” 싶은데, JMM 입장에선 꼭 그렇지 않다.

  • number = 42;ready = true; 순서가 재정렬될 수도 있고,
  • 다른 스레드에서 ready 값이 캐시에 갇혀 메인 메모리 변경을 못 볼 수도 있다.

그래서 실제로는:

  • 0이 출력되거나,
  • while (!ready) 루프에서 영원히 빠져나오지 못하는 것도 허용되는 동작이다.

여기서 readyvolatile을 붙이면 상황이 달라진다.

public class Runner {
    private static int number = 0;
    private static volatile boolean ready = false;

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!ready) {
                // busy-wait
            }
            System.out.println(number);
        });
        t.start();

        number = 42;   // (1)
        ready = true;  // (2) volatile write
    }
}
  • 메인 스레드가 ready = true를 쓰는 순간까지의 모든 변경(여기선 number = 42)은,
  • 다른 스레드가 readytrue로 읽고 난 뒤에는 반드시 보인다.

즉, ready == true라면 number == 42가 보장된다.
이게 volatile이 제공하는 가시성 + 순서 보장의 전형적인 사용 예다.


4. volatile 로 되는 것 vs 안 되는 것

✅ 잘 맞는 사용처: “상태 알려주기”

volatile이 빛나는 곳은 상태를 공유하고 알리는 용도다.

4-1. 종료 플래그

class Worker implements Runnable {
    private volatile boolean running = true;

    public void stop() {
        running = false; // 다른 스레드에서 바로 감지
    }

    @Override
    public void run() {
        while (running) {
            // 어떤 작업들...
        }
    }
}
  • 한 스레드에서 stop()을 호출하면,
  • 다른 스레드의 while (running) 루프가 바로 종료된다.

4-2. 초기화 완료 플래그

class ConfigHolder {
    private static Config config;
    private static volatile boolean initialized = false;

    public static void init() {
        config = loadConfig();  // 무거운 초기화 작업
        initialized = true;     // 이 시점까지의 작업이 다른 스레드에 보장
    }

    public static Config get() {
        if (!initialized) {
            throw new IllegalStateException("Not initialized");
        }
        return config;          // 여기서는 안전하게 사용 가능
    }
}
  • initializedtrue라면, 그 전에 수행된 loadConfig()의 결과도 다른 스레드에서 일관되게 보인다.

❌ 안 맞는 사용처: “같이 수정하는 값”

volatile원자성(atomicity) 을 보장하지 않는다.

class Counter {
    private volatile int count = 0;

    public void increment() {
        count++; // (1) read (2) +1 (3) write
    }
}

count++는 세 단계로 나뉘기 때문에,

  • 여러 스레드가 동시에 increment()를 호출하면,
  • 실제 증가 횟수보다 더 적게 증가하는 전형적인 레이스 컨디션이 발생한다.

이런 경우는:

  • AtomicIntegerincrementAndGet() 같은 원자 연산을 쓰거나,
  • synchronized, ReentrantLock 같은 락을 써야 한다.

정리하면:

  • volatile“보여주는 문제(가시성/순서)”는 해결하지만,
  • “같이 건드리는 문제(원자성/경쟁)”는 해결하지 않는다.

5. 언제 volatile을 쓰면 좋을까? (실무 감각)

실제 코드 짤 때는 대략 이렇게 생각하면 편하다.

써도 좋은 경우

  • 플래그/상태 공유
    • running, stopped, initialized, shutdownRequested 같은 것들
  • 더블 체크 락킹에서 “초기화 완료 여부” 표현
  • 값 자체는 자주 안 바뀌고, 읽기 비율이 훨씬 높은 상태 값 공유

쓰면 위험한 경우

  • 카운터, 합계, 평균 등 여러 스레드가 동시에 수정하는 숫자
  • 큐/리스트/맵 같은 자료구조 직접 구현
  • 여러 필드를 묶어서 “항상 일관된 상태”를 유지해야 하는 복합 상태

이런 건 전부 volatile만으론 부족하고, 락 또는 동시성 컬렉션이 필요하다.


6. 마무리

정리하자면:

  • volatile은 자바 메모리 모델에서 가시성과 순서를 보장해주는 가벼운 동기화 도구다.
  • “값을 최신 상태로 보여주는 것”까지는 책임지지만,
    “여러 스레드가 동시에 수정해도 안전하게 만들기”까지는 책임지지 않는다.
  • 그래서:
    • 플래그/상태 공유에는 잘 맞고,
    • 공유 카운터/자료구조에는 다른 도구(Atomic*, synchronized, Lock)가 필요하다.

[참고]
https://parkcheolu.tistory.com/16
https://programmer-chocho.tistory.com/82
https://www.baeldung.com/java-volatile
https://evanzhao119.github.io/jvm/2025/06/07/java-memory-model-volatile-jmm.html
https://jenkov.com/tutorials/java-concurrency/volatile.html

profile
주니어 백엔드 개발자

0개의 댓글