-
[Java] volatile 예약어Java 2025. 2. 18. 22:31
volatile이란?
volatile 예약어는 자바의 동시성 환경에서 변수의 가시성 문제를 해결하기 위해 사용되는 예약어이다.
volatile의 특징
1. 가시성 보장
- 한 스레드가 volatile 변수를 변경하면, 다른 스레드가 즉시 변경된 값을 읽을 수 있다.
- CPU 캐시를 사용하지 않고, 메인 메모리(RAM)에서 직접 읽고 쓰기 때문이다.
2. 원자성 미보장
- volatile은 읽기/쓰기는 안전하지만, 연산 등과 같은 복합 연산은 원자적으로 동작하지 않는다.
예제 코드
1. 가시성 문제
- volatile 없이 실행했을 때 가시성 문제 발생 가능!
public class NoVolatileMain { private static boolean flag = false; public static void main(String[] args) { Thread writer = new Thread(() -> { try { Thread.sleep(1000); // 1초 후 변경 flag = true; System.out.println("Writer: flag 값을 true로 변경"); } catch (InterruptedException e) { e.printStackTrace(); } }); Thread reader = new Thread(() -> { int i = 0; while (!flag) { // flag가 true가 될 때까지 대기 System.out.println(i++); } System.out.println("Reader: flag가 true로 변경됨을 감지!"); }); writer.start(); reader.start(); } }
- 반면 아래와 같이 volatile 변수를 사용하면 CPU 캐시에서 값을 읽고 쓰는 게 아니라 메인 메모리에 있는 값을 직접 읽고 쓰기 때문에 즉각적인 반응을 기대할 수 있다.
public class VolatileMain { private static volatile boolean flag = false; public static void main(String[] args) { Thread writer = new Thread(() -> { try { Thread.sleep(1000); // 1초 후 변경 flag = true; System.out.println("Writer: flag 값을 true로 변경"); } catch (InterruptedException e) { e.printStackTrace(); } }); Thread reader = new Thread(() -> { int i = 0; while (!flag) { // flag가 true가 될 때까지 대기 System.out.println(i++); } System.out.println("Reader: flag가 true로 변경됨을 감지!"); }); writer.start(); reader.start(); } }
2. volatile의 한계: 원자성 문제
- ++와 같은 복합 연산은 읽기 -> 증가 -> 쓰기, 세 단계로 이루어져 있다.
- volatile은 단순 읽기/쓰기에는 문제 없지만, 복합 연산에는 원자적으로 동작하지 않는다.
- 해결 방법: synchronized 또는 AtomicInteger 사용
public class VolatileProblem { private static volatile int count = 0; public static void main(String[] args) { Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { count++; // 원자적 연산이 아님! } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { count++; } }); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("최종 count 값: " + count); // 예상: 2000, 하지만 실제 값은 불확실 } }
volatile의 원자성 문제 - AtomicInteger를 사용하면 원자성을 보장할 수 있다.
- AtomicInteger의 incrementAndGet()은 원자적으로 증가 연산을 수행시켜 데이터 경합 문제를 해결한다.
public class AtomicIntegerMain { private static AtomicInteger count = new AtomicInteger(0); // AtomicInteger 사용 public static void main(String[] args) { Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { count.incrementAndGet(); // 원자적 연산 보장 } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { count.incrementAndGet(); } }); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("최종 count 값: " + count.get()); // 항상 2000 보장 } }
정리
특징 volatile synchronized AtomicXxx 가시성 보장 Yes Yes Yes 원자성 보장 No Yes Yes 성능 빠름 느림 빠름 재배치 방지 Yes Yes Yes 적합한 상황 단순 읽기/쓰기 동기화 블록 필요 시 숫자 연산 동기화 시 - volatile은 단순 읽기/쓰기 가시성 문제 해결에는 효과적이지만, 연산(++)은 원자적이지 않다.
- synchronized는 원자성 보장이 필요할 때 유용하지만 성능 저하가 발생할 수 있다.
- AtomicInteger는 안전한 숫자 증가 연산이 필요한 경우, 가장 좋은 선택이 될 수 있다.
'Java' 카테고리의 다른 글
[Java] 생산자-소비자 문제 (0) 2025.04.08 [Java] record 예약어 (1) 2025.02.13 [Java] 리플렉션(Reflection)에 대해서 (1) 2025.01.17 [Java] I/O 정리 (1) 2024.11.09 [Java] Charset과 문자 인코딩 (0) 2024.11.08