ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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