volatile은 Java 변수를 Main Memory에 저장하겠다는 것을 명시하는 키워드이다. 즉, 매번 변수의 값을 Read할 때마다 CPU Cache가 아닌 Main Memory에서 읽고, Write할 때마다 Main Memory에 작성하는 것이다.
현대의 컴퓨터는 성능 향상을 위해 CPU Cache를 사용한다. CPU는 Main Memory보다 CPU Cache에 훨씬 빠르게 접근할 수 있기 때문에, 변수를 Main Memory에서 읽어와 CPU Cache에 저장하고, 이후에는 CPU Cache에서 변수를 읽어 처리한다.

[Thread 1] -----> [CPU Cache 1] -----> [Main Memory]
[Thread 2] -----> [CPU Cache 2] -----> [Main Memory]
Multi-Thread 환경에서는 각 Thread가 변수를 CPU Cache에 저장하게 되는데, 이로 인해 가시성(Visibility) 문제가 발생한다.
public class SharedObject {
public int counter = 0;
}
volatile 키워드가 붙은 변수는 CPU Cache를 거치지 않고 Main Memory에서 직접 읽고 쓴다.
public class SharedObject {
public volatile int counter = 0;
}
한 Thread에서 volatile 변수를 수정하면, 다른 Thread에서 즉시 변경된 값을 볼 수 있다.
[Thread 1] -----> [Main Memory] <----- [Thread 2]
(직접 읽기/쓰기)
public class VolatileExample {
private boolean running = true; // volatile 없음
public void stop() {
running = false;
System.out.println("Stop 호출됨");
}
public void run() {
System.out.println("Thread 시작");
while (running) {
// 작업 수행
// Thread가 멈추지 않을 수 있음!
}
System.out.println("Thread 종료");
}
public static void main(String[] args) throws InterruptedException {
VolatileExample example = new VolatileExample();
Thread thread = new Thread(example::run);
thread.start();
Thread.sleep(1000);
example.stop(); // running을 false로 변경
// 하지만 thread는 계속 실행될 수 있음!
}
}
public class VolatileExample {
private volatile boolean running = true; // volatile 추가
public void stop() {
running = false;
System.out.println("Stop 호출됨");
}
public void run() {
System.out.println("Thread 시작");
while (running) {
// 작업 수행
}
System.out.println("Thread 종료"); // 정상적으로 종료됨
}
public static void main(String[] args) throws InterruptedException {
VolatileExample example = new VolatileExample();
Thread thread = new Thread(example::run);
thread.start();
Thread.sleep(1000);
example.stop(); // Thread가 정상적으로 종료됨
}
}
public class Singleton {
private static Singleton instance; // volatile 없음
public static Singleton getInstance() {
if (instance == null) { // 첫 번째 체크
synchronized (Singleton.class) {
if (instance == null) { // 두 번째 체크
instance = new Singleton();
// 문제: 객체 생성이 완료되기 전에
// 다른 Thread가 instance를 읽을 수 있음
}
}
}
return instance;
}
}
public class Singleton {
private static volatile Singleton instance; // volatile 추가
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
// volatile로 인해 완전히 초기화된 객체만 보임
}
}
}
return instance;
}
}
public class TaskRunner {
private volatile boolean isReady = false;
private volatile boolean isDone = false;
public void prepareTask() {
// 준비 작업 수행
System.out.println("작업 준비 중...");
isReady = true; // 다른 Thread에서 즉시 확인 가능
}
public void executeTask() {
while (!isReady) {
// isReady가 true가 될 때까지 대기
}
System.out.println("작업 실행 중...");
// 실제 작업 수행
isDone = true; // 작업 완료 표시
}
public void checkStatus() {
while (!isDone) {
// 작업 완료 대기
}
System.out.println("작업 완료 확인!");
}
}
public class ProducerConsumer {
private volatile int data = 0;
private volatile boolean hasData = false;
// Producer Thread
public void produce(int value) {
while (hasData) {
// Consumer가 데이터를 소비할 때까지 대기
}
data = value;
hasData = true; // 데이터 준비 완료
System.out.println("생산: " + value);
}
// Consumer Thread
public int consume() {
while (!hasData) {
// Producer가 데이터를 생산할 때까지 대기
}
int value = data;
hasData = false; // 데이터 소비 완료
System.out.println("소비: " + value);
return value;
}
}
volatile은 단순 읽기/쓰기에 대해서만 원자성을 보장counter++)은 원자성을 보장하지 않음public class VolatileCounter {
private volatile int counter = 0;
public void increment() {
counter++; // 원자적이지 않음!
// 실제로는 3단계: 1. 읽기, 2. 증가, 3. 쓰기
// 여러 Thread가 동시에 실행하면 값이 손실될 수 있음
}
}
volatile 변수에 대한 쓰기 작업은 이후의 읽기 작업보다 먼저 발생함을 보장public class HappensBeforeExample {
private int normalVariable = 0;
private volatile boolean flag = false;
// Thread 1
public void writer() {
normalVariable = 42; // 1. 일반 변수 쓰기
flag = true; // 2. volatile 변수 쓰기
}
// Thread 2
public void reader() {
if (flag) { // 3. volatile 변수 읽기
// normalVariable의 값이 42임이 보장됨
System.out.println(normalVariable); // 4. 일반 변수 읽기
}
}
}
| 특징 | volatile | synchronized |
|---|---|---|
| 가시성 보장 | O | O |
| 원자성 보장 | X (단순 읽기/쓰기만 O) | O |
| 성능 | 빠름 (Lock 없음) | 느림 (Lock 사용) |
| 사용 범위 | 변수에만 사용 | 메서드, 블록에 사용 |
| Block | 하지 않음 | 할 수 있음 |
public class VolatileUsage {
// 1. Flag 변수 (단순 상태 체크)
private volatile boolean isRunning = true;
// 2. 최근 값 저장 (읽기만 하는 경우)
private volatile long lastUpdateTime;
// 3. 참조 타입 (객체 교체)
private volatile Configuration config;
}
public class SynchronizedUsage {
private int counter = 0; // volatile로는 불충분
// 복합 연산은 synchronized 필요
public synchronized void increment() {
counter++; // 읽기 + 증가 + 쓰기 (3단계)
}
// 또는 AtomicInteger 사용
private AtomicInteger atomicCounter = new AtomicInteger(0);
public void incrementAtomic() {
atomicCounter.incrementAndGet(); // 원자적 연산
}
}
public class VolatileProblem {
private volatile int count = 0;
public void increment() {
count++; // Race Condition 발생 가능!
// 1. count 읽기
// 2. count + 1 계산
// 3. 결과를 count에 쓰기
// → 3단계가 원자적이지 않음
}
}
public class SynchronizedSolution {
private int count = 0;
public synchronized void increment() {
count++; // 원자적으로 실행됨
}
}
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicSolution {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 원자적으로 실행됨
}
public int get() {
return count.get();
}
}
public class VolatileInconsistency {
private volatile int x = 0;
private volatile int y = 0;
public void update() {
x = 10; // Thread 1이 여기까지 실행
// Thread 2가 여기서 x, y를 읽으면 일관성 깨짐
y = 20; // Thread 1이 여기 실행
}
}
public class SynchronizedConsistency {
private int x = 0;
private int y = 0;
public synchronized void update() {
x = 10;
y = 20; // x, y가 함께 업데이트됨을 보장
}
public synchronized void read() {
System.out.println("x: " + x + ", y: " + y);
// 일관된 상태를 읽음
}
}
public class ServiceManager {
private volatile boolean isServiceActive = false;
public void startService() {
// 서비스 시작 로직
System.out.println("서비스 시작 중...");
// 초기화 작업...
isServiceActive = true; // 모든 Thread에게 즉시 알림
System.out.println("서비스 활성화 완료");
}
public void stopService() {
isServiceActive = false; // 모든 Thread에게 즉시 알림
System.out.println("서비스 비활성화");
}
public void processRequest() {
if (!isServiceActive) {
throw new IllegalStateException("서비스가 활성화되지 않음");
}
// 요청 처리...
}
}
public class ConfigurationManager {
private volatile Configuration currentConfig;
public ConfigurationManager() {
this.currentConfig = loadConfiguration();
}
public void reloadConfiguration() {
Configuration newConfig = loadConfiguration();
currentConfig = newConfig; // 원자적 교체, 즉시 가시적
System.out.println("설정 리로드 완료");
}
public Configuration getConfiguration() {
return currentConfig; // 항상 최신 설정 반환
}
private Configuration loadConfiguration() {
// 설정 파일에서 로드
return new Configuration();
}
}
class Configuration {
private final String dbUrl;
private final int timeout;
// 불변 객체로 구성
public Configuration() {
this.dbUrl = "jdbc:mysql://localhost:3306/db";
this.timeout = 3000;
}
}
public class CacheManager {
private volatile boolean cacheInvalid = false;
private Map cache = new ConcurrentHashMap<>();
public Object get(String key) {
if (cacheInvalid) {
refreshCache();
}
return cache.get(key);
}
public void invalidateCache() {
cacheInvalid = true; // 모든 Thread에게 즉시 알림
}
private synchronized void refreshCache() {
if (cacheInvalid) { // Double-checked locking
// 캐시 갱신 로직
cache.clear();
// 새로운 데이터 로드...
cacheInvalid = false;
}
}
}
public class PerformanceComparison {
private int normal = 0;
private volatile int volatileVar = 0;
private int synchronizedVar = 0;
// 가장 빠름
public void normalAccess() {
int value = normal;
}
// 중간 속도
public void volatileAccess() {
int value = volatileVar;
}
// 가장 느림
public synchronized void synchronizedAccess() {
int value = synchronizedVar;
}
}