목차
volatile 키워드
한 스레드가 작업을 멈추라는 신호를 보내고 다른 스레드가 그 신호를 읽는다고 해보자. 두 스레드가 같은 필드를 읽고 쓰더라도 동기화 관계가 없다면 읽는 쪽이 변경된 값을 언제 볼지 보장할 수 없다. volatile은 이런 단일 필드의 상태 전달에 쓸 수 있다.
final class StopSignal {
private volatile boolean requested;
void requestStop() { requested = true; }
boolean isRequested() { return requested; }
}
한 스레드가 requestStop()을 호출하고 다른 스레드가 isRequested()를 읽는 상황이다. 실제 작업에서는 플래그만 계속 확인하는 바쁜 대기(busy waiting)를 피하고, 상황에 따라 인터럽트나 차단 큐를 선택한다.
가시성과 순서
Java 메모리 모델에서 한 volatile 필드에 대한 쓰기는 그 뒤의 같은 필드 읽기에 대해 happens-before 관계를 만든다. 쓰기 전에 수행한 작업도 읽기 이후의 작업에 보이도록 하는 순서·가시성 보장이 핵심이다. 이를 “항상 메인 메모리에서 읽고 CPU 캐시를 사용하지 않는다”라고 설명하면 부정확하다. CPU와 JVM의 캐시 구현보다 언어가 보장하는 관찰 가능한 결과를 기준으로 이해해야 한다.
sequenceDiagram
participant A as 작업 스레드 A
participant F as volatile 플래그
participant B as 작업 스레드 B
A->>A: 공유 데이터 준비
A->>F: ready = true 쓰기
B->>F: ready 읽기
F-->>B: true
B->>B: 준비된 데이터 사용
A가 데이터를 준비하고 ready를 마지막에 쓰는 게시(publish) 패턴을 단순화한 그림이다. B가 ready == true를 읽으면 A가 그 전에 한 작업을 볼 수 있다. 공유 객체를 이후에도 여러 스레드가 계속 바꾼다면 별도의 동기화 규칙이 필요하다.
복합 연산은 원자적이지 않다
volatile은 읽기와 쓰기를 묶은 연산을 한 번에 처리하지 않는다.
final class UnsafeCounter {
private volatile int count;
void increment() {
count++; // 읽기 → 더하기 → 쓰기
}
}
두 스레드가 모두 count=0을 읽고 각자 1을 쓰면 결과는 1이다. volatile은 각 접근의 가시성을 보장하지만 그 사이에 다른 스레드가 끼어들지 못하게 하는 락은 아니다. 이런 카운터에는 AtomicInteger.incrementAndGet() 또는 같은 락으로 보호한 synchronized 블록을 사용한다.
import java.util.concurrent.atomic.AtomicInteger;
final class SafeCounter {
private final AtomicInteger count = new AtomicInteger();
int increment() { return count.incrementAndGet(); }
int get() { return count.get(); }
}
| 필요한 동작 | 우선 고려할 도구 |
|---|---|
| 단일 상태 플래그 전달 | volatile 또는 목적에 맞는 동시성 도구 |
| 한 숫자를 원자적으로 증가 | AtomicInteger |
| 여러 필드의 규칙을 함께 유지 | 같은 락을 쓰는 synchronized 등 |
| 작업 취소·대기 중 깨우기 | 인터럽트 또는 작업 실행 API |
long과 double은 어떻게 다른가
오래된 설명처럼 “JVM이 4바이트씩 처리하므로 long과 double은 언제나 둘로 쪼개진다”라고 단정할 수 없다. Java 언어 명세는 일반 long/double의 읽기·쓰기가 원자적일 것을 요구하지 않는다고 규정한다. 특정 JVM과 하드웨어의 구현은 별개다. volatile long과 volatile double의 개별 읽기·쓰기는 원자적이다. 그래도 value++는 읽기와 쓰기를 묶은 복합 연산이므로 원자적이지 않다.
final도 volatile을 대체하는 만능 표시는 아니다. final 필드는 생성 후 재대입할 수 없고 초기화와 관련된 가시성 보장이 있지만, 그 필드가 가리키는 변경 가능한 객체의 내부 상태까지 자동으로 스레드 안전하게 만들지는 않는다.
결국 필드 하나의 새 값을 알리는 일인지, 아니면 여러 단계의 상태 변경을 한 덩어리로 묶어야 하는지 먼저 구분해야 한다.
참고: Java 언어 명세의 메모리 모델, Oracle의 원자적 접근 설명, AtomicInteger API