자바 volatile = 멀티스레드를 공부하다보면 마주치는 생소한 키워드 😅
volatile 을 써야 할까?멀티 스레드 코드를 짜다 보면, 가끔 이런 기분 나쁜 일이 생긴다.
이유는 단순하다.
그 결과, 같은 변수를 공유하는데도 스레드마다 서로 다른 값을 보고 있는 상황이 생긴다.
이게 바로 메모리 가시성(visibility) 문제다.
volatile 이 뭔데?volatile을 한 줄로 요약하면 이렇게 말할 수 있다.
“이 변수는 여러 스레드가 같이 보니까,
항상 최신 값을 메인 메모리 기준으로 보게 하고, 앞뒤 순서도 꼬지 마”
조금 더 공식적으로 말하면, volatile은 두 가지를 보장한다.
가시성(Visibility)
volatile 변수에 쓴 값은 즉시 메인 메모리에 반영된다.명령 재정렬 방지(Happens-before)
volatile write 이전에 일어난 일들은,volatile read 한 이후에는 이미 일어난 것으로 보장된다.즉, 단순히 “캐시 쓰지 마” 수준이 아니라,
해당 변수를 기준으로 메모리 배리어를 세우는 동기화 도구라고 보면 된다.
먼저 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 값이 캐시에 갇혀 메인 메모리 변경을 못 볼 수도 있다.그래서 실제로는:
while (!ready) 루프에서 영원히 빠져나오지 못하는 것도 허용되는 동작이다.여기서 ready에 volatile을 붙이면 상황이 달라진다.
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)은,ready를 true로 읽고 난 뒤에는 반드시 보인다.즉, ready == true라면 number == 42가 보장된다.
이게 volatile이 제공하는 가시성 + 순서 보장의 전형적인 사용 예다.
volatile 로 되는 것 vs 안 되는 것volatile이 빛나는 곳은 상태를 공유하고 알리는 용도다.
class Worker implements Runnable {
private volatile boolean running = true;
public void stop() {
running = false; // 다른 스레드에서 바로 감지
}
@Override
public void run() {
while (running) {
// 어떤 작업들...
}
}
}
stop()을 호출하면,while (running) 루프가 바로 종료된다.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; // 여기서는 안전하게 사용 가능
}
}
initialized가 true라면, 그 전에 수행된 loadConfig()의 결과도 다른 스레드에서 일관되게 보인다.volatile은 원자성(atomicity) 을 보장하지 않는다.
class Counter {
private volatile int count = 0;
public void increment() {
count++; // (1) read (2) +1 (3) write
}
}
count++는 세 단계로 나뉘기 때문에,
increment()를 호출하면,이런 경우는:
AtomicInteger의 incrementAndGet() 같은 원자 연산을 쓰거나,synchronized, ReentrantLock 같은 락을 써야 한다.정리하면:
volatile은 “보여주는 문제(가시성/순서)”는 해결하지만,volatile을 쓰면 좋을까? (실무 감각)실제 코드 짤 때는 대략 이렇게 생각하면 편하다.
running, stopped, initialized, shutdownRequested 같은 것들이런 건 전부 volatile만으론 부족하고, 락 또는 동시성 컬렉션이 필요하다.
정리하자면:
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