4주차 Unit 4.5 — volatile (메모리 가시성)

Psj·2026년 5월 21일

F-lab

목록 보기
136/239

Unit 4.5 — volatile (메모리 가시성)

F-LAB JAVA · 4주차 · Phase 4 · 동기화: synchronized와 메모리 가시성
★ 마스터 Unit + 🏆 Phase 4 완주 — ★ 4주차 1차 정점 완료


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 메모리 가시성 (Visibility) 문제 의 정의는?
  • runFlag 가 영원히 종료 안 되는 시나리오는?
  • CPU 캐시 와 메인 메모리의 관계는?
  • volatile 키워드 의 효과는?
  • volatile 이 원자성을 보장하지 않는 이유는?
  • count++ 가 volatile 로도 위험한 이유는?
  • volatile vs synchronized 의 차이는?
  • happens-before 관계는?
  • volatile 의 적절한 사용 사례 는?

🎯 핵심 한 문장

메모리 가시성 (Visibility) 문제는 한 스레드의 변경이 다른 스레드에 보이지 않는 현상이며, 각 CPU 코어가 자신의 캐시 메모리를 사용하기 때문에 발생한다.
예를 들어 한 스레드가 runFlag = false 로 변경해도 다른 스레드의 캐시에는 여전히 true 가 남아 while (runFlag) 루프가 영원히 종료되지 않을 수 있다.
volatile 키워드 는 해당 변수의 읽기·쓰기를 항상 메인 메모리에 직접 수행하도록 하여 (캐시 우회) 가시성을 보장한다 — 약간 느려지지만 변경이 즉시 모든 스레드에 보인다.
단, volatile 은 원자성을 보장하지 않는다count++ 는 여전히 읽기-수정-쓰기 3단계이므로 가시성은 보장되어도 경쟁 조건 (lost update) 은 발생한다.
volatile 은 가시성만, synchronized 는 가시성 + 원자성 을 보장하므로, 단순 플래그 (boolean) 에는 volatile, 복합 연산 (count++) 에는 synchronized/Atomic 을 사용한다.

비유 — 게시판 vs 개인 수첩

가시성 문제 = 각자 수첩에 적기:

메인 메모리 = 중앙 게시판
CPU 캐시 = 각자 개인 수첩

문제:
  - 스레드 A: 게시판 보고 수첩에 "운영중=true" 적음
  - 스레드 B: 게시판에 "운영중=false" 적음
  - 하지만 A 는 자기 수첩 (true) 만 봄
  - A 는 영원히 "운영중" 으로 착각

volatile = 항상 게시판 직접 확인:
  - 수첩 안 씀
  - 매번 게시판 (메인 메모리) 읽기/쓰기
  - 변경 즉시 모두 봄

원자성 X:
  - 게시판 직접 봐도
  - "읽고-더하고-쓰기" 사이 다른 사람 개입 가능
  - count++ 손실

→ volatile = 항상 메인 메모리 직접 (가시성), 단 원자성 X.


🧭 9개 섹션 로드맵

1. 메모리 가시성 문제
2. runFlag 무한 루프 시나리오
3. CPU 캐시와 메인 메모리
4. volatile의 효과
5. volatile은 원자성 보장 X
6. count++가 volatile로도 위험
7. volatile vs synchronized
8. happens-before와 적절한 사용
9. 면접 + 자기 점검 + 마스터 50문항 + Phase 4 완주

1️⃣ 메모리 가시성 문제

1.1 가시성 문제란

메모리 가시성 (Visibility) 문제:

  한 스레드의 변경이
  다른 스레드에 보이지 않는 현상.

원인:
  - 각 CPU 코어가 자기 캐시
  - 변경이 캐시에만 (메인 메모리 X)
  - 다른 코어는 옛 값

1.2 발생 메커니즘

가시성 문제 발생:

스레드 A (코어 1):
  변수 x = 10 으로 변경
  → 코어 1 캐시에 10
  → 메인 메모리 반영 지연

스레드 B (코어 2):
  변수 x 읽기
  → 코어 2 캐시의 옛 값 (예: 0)
  → A 의 변경 못 봄

1.3 시각화

가시성 문제:

         메인 메모리
         x = 0 (옛 값)
        /          \
  코어1 캐시      코어2 캐시
  x = 10          x = 0
  (A 변경)        (B 가 봄)
       ↑              ↑
   스레드 A        스레드 B

  A 변경이 B 에 안 보임

1.4 컴파일러 최적화

가시성 문제의 또 다른 원인 — 최적화:

컴파일러/JIT 최적화:
  - 반복 읽는 변수를 레지스터에 캐싱
  - 매번 메모리 안 읽음
  - 변경 감지 못함

예:
  while (flag) { }
  → flag 를 레지스터에 (한 번 읽고 재사용)
  → flag 변경 감지 X
  → 무한 루프

1.5 가시성 vs 원자성

가시성 vs 원자성:

가시성 (Visibility):
  - 변경이 보이는가
  - volatile 해결

원자성 (Atomicity):
  - 연산 분리 안 됨
  - synchronized/Atomic 해결

count++ 는 둘 다 문제:
  - 가시성 (변경 안 보임)
  - 원자성 (3단계)

1.6 ILIC 의 맥락

public class VisibilityProblem {
    
    private boolean running = true;   // 가시성 문제
    
    public void start() {
        Thread worker = new Thread(() -> {
            while (running) {   // 캐시의 옛 값 (true) 만 봄
                process();
            }
            // running = false 변경을 못 볼 수 있음
            // → 영원히 루프
        });
        worker.start();
        
        sleep(1000);
        running = false;   // 변경 (worker 가 못 볼 수 있음)
    }
    
    private void process() { }
}

1.7 자기 점검 답변

메모리 가시성 문제의 정의는?

:
1. 정의:

  • 변경이 다른 스레드에 안 보임
  1. 원인:

    • CPU 캐시 (각 코어)
    • 컴파일러 최적화
  2. 메커니즘:

    • 변경이 캐시에만
    • 다른 코어는 옛 값
  3. vs 원자성:

    • 가시성: 보이는가
    • 원자성: 분리 안 됨

2️⃣ runFlag 무한 루프 시나리오

2.1 고전 예제

public class StopThread {
    private boolean runFlag = true;   // ★ 가시성 문제
    
    public void start() throws InterruptedException {
        Thread worker = new Thread(() -> {
            int count = 0;
            while (runFlag) {   // 캐시의 true 만
                count++;
            }
            System.out.println("Stopped: " + count);
        });
        worker.start();
        
        Thread.sleep(1000);
        runFlag = false;   // 변경
        System.out.println("Set runFlag = false");
        
        // worker 가 멈출까?
        // → 멈추지 않을 수 있음 (가시성 문제)
        // → 무한 루프
    }
}

2.2 왜 무한 루프

무한 루프 이유:

1. CPU 캐시:
   - worker 가 runFlag 를 캐시에
   - main 의 false 변경이 캐시 미반영
   - worker 는 true 계속 봄

2. 컴파일러 최적화:
   - while(runFlag) 에서
   - runFlag 를 레지스터에 캐싱
   - 매번 안 읽음
   - 변경 감지 X

→ 멈추지 않음

2.3 시각화

무한 루프:

main:    runFlag = false (메인 메모리)
                   ↓ (worker 캐시 미반영)
worker:  while(runFlag)  ← 캐시의 true
         while(true)     ← 영원히
         while(true)
         ...무한...

2.4 volatile 로 해결

public class StopThreadFixed {
    private volatile boolean runFlag = true;   // ★ volatile
    
    public void start() throws InterruptedException {
        Thread worker = new Thread(() -> {
            while (runFlag) {   // 항상 메인 메모리
                // ...
            }
            System.out.println("Stopped");
        });
        worker.start();
        
        Thread.sleep(1000);
        runFlag = false;   // 메인 메모리에 즉시
        // worker 가 즉시 봄 → 멈춤
    }
}

2.5 실행마다 다름

가시성 문제의 비결정성:

  같은 코드 실행:
    - 어떤 환경: 멈춤 (운 좋게)
    - 다른 환경: 무한 루프

  의존 요소:
    - JVM 구현
    - CPU 아키텍처
    - 최적화 수준
    - 부하

→ "내 PC 에선 됐는데" 함정
→ volatile 로 확실히

2.6 ILIC 의 맥락

@Service
public class ShipmentWorkerControl {
    
    // ❌ 가시성 문제
    private boolean stopRequested = false;
    
    // ✓ volatile
    private volatile boolean stopRequestedSafe = false;
    
    public void startWorker() {
        Thread worker = new Thread(() -> {
            while (!stopRequestedSafe) {   // volatile (즉시 반영)
                processNextShipment();
            }
            log.info("Worker stopped gracefully");
        });
        worker.start();
    }
    
    public void requestStop() {
        stopRequestedSafe = true;   // 즉시 worker 에 보임
    }
    
    private void processNextShipment() { }
}

2.7 자기 점검 답변

runFlag가 영원히 종료 안 되는 시나리오는?

:
1. 시나리오:

  • while(runFlag) 루프
  • 다른 스레드가 false
  • 변경 안 보임 → 무한
  1. 이유:

    • CPU 캐시 (옛 값)
    • 컴파일러 최적화 (레지스터)
  2. 비결정성:

    • 환경마다 다름
    • "내 PC 에선 됐는데"
  3. 해결:

    • volatile

3️⃣ CPU 캐시와 메인 메모리

3.1 메모리 계층

메모리 계층 (속도 vs 크기):

레지스터 (가장 빠름)
   ↓
L1 캐시 (~1ns)
   ↓
L2 캐시 (~4ns)
   ↓
L3 캐시 (~10ns, 공유)
   ↓
메인 메모리 (~100ns)

  - 캐시: 코어별 (L1, L2)
  - 메인 메모리: 공유

3.2 캐시의 목적

캐시의 목적:

  메인 메모리 접근은 느림 (~100ns).
  자주 쓰는 데이터를 캐시에 (빠름).

효과:
  - 성능 ↑
  - 메모리 접근 줄임

문제:
  - 코어별 캐시
  - 일관성 (가시성)

3.3 코어별 캐시

코어별 캐시:

  코어1: L1, L2 (자기 것)
  코어2: L1, L2 (자기 것)
  공유: L3, 메인 메모리

문제:
  - 코어1 이 변수 변경 → 자기 캐시
  - 코어2 는 자기 캐시 (옛 값)
  - 불일치 (가시성 문제)

3.4 캐시 일관성 프로토콜

캐시 일관성 (Cache Coherence):

  MESI 프로토콜 등:
    - Modified, Exclusive, Shared, Invalid
    - 캐시 라인 상태 관리

  하지만:
    - 즉시 반영 보장 X (성능)
    - 메모리 배리어 필요
    - volatile 이 배리어 삽입

3.5 메모리 배리어

메모리 배리어 (Memory Barrier):

  CPU 명령 재정렬과 캐시 동기화 제어.

volatile:
  - 쓰기 후 store 배리어 (flush)
  - 읽기 전 load 배리어 (refresh)
  - 메인 메모리 직접

효과:
  - 가시성 보장
  - 재정렬 방지

3.6 ILIC 의 맥락

public class CacheExample {
    
    // 일반 변수 — 캐시 가능
    private int normalCounter = 0;
    
    // volatile — 메인 메모리 직접
    private volatile int volatileCounter = 0;
    private volatile boolean flag = false;
    
    public void demonstrate() {
        // normalCounter: 캐시될 수 있음 (가시성 X)
        normalCounter++;
        
        // volatileCounter: 항상 메인 메모리 (가시성 O)
        // 단, ++ 는 원자성 X (다음 섹션)
        
        // flag: 가시성 보장 (단순 플래그에 적합)
        flag = true;   // 즉시 모든 스레드에
    }
}

3.7 자기 점검 답변

CPU 캐시와 메인 메모리의 관계는?

:
1. 계층:

  • 레지스터 → L1/L2/L3 → 메인
  • 캐시: 빠르지만 코어별
  1. 목적:

    • 성능 (느린 메모리 보완)
  2. 문제:

    • 코어별 캐시
    • 불일치 (가시성)
  3. 해결:

    • 메모리 배리어
    • volatile (메인 직접)

4️⃣ volatile의 효과

4.1 volatile 키워드

private volatile boolean flag = false;
private volatile int counter = 0;

// volatile 변수:
// - 읽기/쓰기 항상 메인 메모리
// - 캐시 우회
// - 가시성 보장

4.2 두 가지 효과

volatile 의 두 가지 효과:

1. 가시성 (Visibility)
   - 변경이 즉시 모든 스레드에
   - 메인 메모리 직접

2. 순서 보장 (Ordering)
   - 재정렬 방지 (메모리 배리어)
   - happens-before

4.3 가시성 보장

volatile 가시성:

쓰기:
  - volatile 변수 쓰기
  - 즉시 메인 메모리 flush
  - 캐시 무효화

읽기:
  - volatile 변수 읽기
  - 메인 메모리에서 직접
  - 최신 값 보장

4.4 재정렬 방지

// 재정렬 문제
public class Reordering {
    private int x = 0;
    private boolean ready = false;
    
    // 스레드 A
    public void writer() {
        x = 42;          // (1)
        ready = true;    // (2)
        // 재정렬 가능: (2) 가 (1) 보다 먼저?
    }
    
    // 스레드 B
    public void reader() {
        if (ready) {     // (3)
            System.out.println(x);   // (4): 42? 0?
            // 재정렬되면 0 가능
        }
    }
}

// volatile 로 해결
private volatile boolean ready = false;
// ready 쓰기 전 모든 쓰기가 먼저 (happens-before)

4.5 volatile 의 비용

volatile 의 비용:

  - 캐시 우회 (메인 메모리 접근)
  - 메모리 배리어
  - 약간 느림 (일반 변수보다)

하지만:
  - synchronized 보다 가벼움
  - 락 없음
  - 단순 가시성에 적합

4.6 ILIC 의 맥락

@Service
public class VolatileEffects {
    
    // 1. 플래그 (가시성)
    private volatile boolean shutdownRequested = false;
    
    public void requestShutdown() {
        shutdownRequested = true;   // 즉시 보임
    }
    
    public boolean isShutdownRequested() {
        return shutdownRequested;   // 최신 값
    }
    
    // 2. 상태 변수 (가시성)
    private volatile ServiceStatus status = ServiceStatus.STARTING;
    
    public void updateStatus(ServiceStatus newStatus) {
        status = newStatus;   // 즉시 모든 스레드에
    }
    
    // 3. 설정 참조 (불변 객체 + volatile)
    private volatile ShipmentConfig config;
    
    public void reloadConfig(ShipmentConfig newConfig) {
        config = newConfig;   // 원자적 참조 교체 + 가시성
    }
    
    enum ServiceStatus { STARTING, RUNNING, STOPPING }
}

4.7 자기 점검 답변

volatile의 효과는?

:
1. 가시성:

  • 메인 메모리 직접
  • 변경 즉시 보임
  1. 순서 보장:

    • 재정렬 방지
    • happens-before
  2. 동작:

    • 쓰기: flush
    • 읽기: 최신
  3. 비용:

    • 캐시 우회
    • synchronized 보다 가벼움

5️⃣ volatile은 원자성 보장 X

5.1 핵심 — 원자성 X

volatile 은 원자성 보장 X:

  가시성만 보장.
  원자성은 보장 안 함.

따라서:
  - 단순 읽기/쓰기: 안전
  - 복합 연산 (count++): 위험

5.2 count++ 의 문제

private volatile int count = 0;

public void increment() {
    count++;   // ★ volatile 이어도 위험!
    // 1. 읽기 (메인 메모리 — 가시성 O)
    // 2. 더하기
    // 3. 쓰기 (메인 메모리 — 가시성 O)
    // 하지만 1~3 사이 다른 스레드 개입 (원자성 X)
}

5.3 왜 위험한가

volatile count++ 위험:

  volatile 은 각 읽기/쓰기를 메인 메모리로.
  하지만 read-modify-write 전체를 원자적으로 X.

  스레드 A: 읽기 (0, 메인 메모리)
  스레드 B: 읽기 (0, 메인 메모리)  ← A 아직 안 씀
  A: +1 → 쓰기 (1)
  B: +1 → 쓰기 (1)  ← 손실

  가시성은 O (둘 다 메인 메모리)
  하지만 원자성 X (손실)

5.4 데모

public class VolatileNotAtomic {
    private volatile int count = 0;
    
    public static void main(String[] args) throws InterruptedException {
        VolatileNotAtomic demo = new VolatileNotAtomic();
        
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    demo.count++;   // volatile 이어도 손실
                }
            });
            t.start();
            threads.add(t);
        }
        for (Thread t : threads) t.join();
        
        // 기대: 100,000
        // 실제: < 100,000 (volatile 이어도 손실!)
        System.out.println("Count: " + demo.count);
    }
}

5.5 원자성 해결

// ❌ volatile (원자성 X)
private volatile int count = 0;
public void inc() { count++; }   // 손실

// ✓ AtomicInteger (원자성 O)
private final AtomicInteger atomicCount = new AtomicInteger();
public void incAtomic() { atomicCount.incrementAndGet(); }

// ✓ synchronized (원자성 O)
private int syncCount = 0;
public synchronized void incSync() { syncCount++; }

5.6 ILIC 의 맥락

@Service
public class VolatileAtomicityExample {
    
    // ❌ volatile 카운터 (원자성 X)
    private volatile int wrongCounter = 0;
    public void incrementWrong() {
        wrongCounter++;   // 손실 가능
    }
    
    // ✓ volatile 플래그 (단순 쓰기, 안전)
    private volatile boolean active = false;
    public void setActive(boolean value) {
        active = value;   // 단순 쓰기 (원자적)
    }
    
    // ✓ AtomicInteger (복합 연산)
    private final AtomicInteger correctCounter = new AtomicInteger();
    public void incrementCorrect() {
        correctCounter.incrementAndGet();   // 원자적
    }
    
    // 정리:
    // - 단순 플래그/참조: volatile
    // - 증감/복합: Atomic 또는 synchronized
}

5.7 자기 점검 답변

volatile이 원자성을 보장하지 않는 이유는?

:
1. 핵심:

  • 가시성만
  • 원자성 X
  1. count++ 위험:

    • 각 읽기/쓰기는 메인 메모리
    • 하지만 RMW 전체는 비원자
  2. 시나리오:

    • 둘 다 읽고 덮어씀
    • 손실 (가시성 O 여도)
  3. 해결:

    • AtomicInteger
    • synchronized

6️⃣ count++가 volatile로도 위험

6.1 가시성 ≠ 원자성

가시성과 원자성 분리:

  volatile count++:
    - 가시성: O (메인 메모리)
    - 원자성: X (3단계)

  → 가시성 있어도 손실

핵심:
  - 두 개념 별개
  - volatile 은 가시성만

6.2 단계별 분석

volatile count++ 단계:

1. count 읽기 (메인 메모리 — 가시성 O)
2. +1 (레지스터)
3. count 쓰기 (메인 메모리 — 가시성 O)

문제:
  - 1과 3 사이 시간
  - 다른 스레드가 1~3 실행
  - 둘 다 같은 값 읽음
  - 원자성 X

6.3 시각화

volatile count++ 손실:

스레드 A          스레드 B          메인 메모리
 읽기(0)                             0
                  읽기(0)            0  ← 둘 다 0
 +1 (1)
                  +1 (1)
 쓰기(1)                             1
                  쓰기(1)            1  ← A의 1 덮어씀

  가시성 O (메인 메모리 읽기/쓰기)
  하지만 손실 (원자성 X)

6.4 단순 쓰기는 안전

// volatile 단순 쓰기 (안전)
private volatile boolean flag;
flag = true;   // 단일 쓰기 (원자적 + 가시성)

private volatile int value;
value = 42;    // 단일 쓰기 (안전)

// volatile 복합 연산 (위험)
value++;       // RMW (위험)
value += 5;    // RMW (위험)
value = value * 2;   // RMW (위험)

6.5 volatile 적합/부적합

volatile 적합:
  ✓ boolean 플래그
  ✓ 단순 상태 변수
  ✓ 참조 교체 (불변 객체)
  ✓ 한 스레드만 쓰고 여러 스레드 읽기

volatile 부적합:
  ✗ 카운터 (count++)
  ✗ 복합 연산
  ✗ 여러 변수 일관성
  ✗ check-then-act

6.6 ILIC 의 맥락

@Service
public class VolatileUsageGuide {
    
    // ✓ 적합 — 플래그
    private volatile boolean running = true;
    
    // ✓ 적합 — 상태 (단일 쓰기)
    private volatile String currentStatus = "IDLE";
    
    // ✓ 적합 — 참조 교체
    private volatile ShipmentConfig config;
    
    // ❌ 부적합 — 카운터 (RMW)
    // private volatile int count;   // count++ 위험
    private final AtomicInteger count = new AtomicInteger();   // 대신 Atomic
    
    public void updateStatus(String status) {
        currentStatus = status;   // ✓ 단일 쓰기 (안전)
    }
    
    public void increment() {
        count.incrementAndGet();   // ✓ Atomic (volatile 대신)
    }
    
    public void stop() {
        running = false;   // ✓ 플래그 (안전)
    }
}

6.7 자기 점검 답변

count++가 volatile로도 위험한 이유는?

:
1. 가시성 ≠ 원자성:

  • volatile: 가시성만
  • count++: 원자성 필요
  1. 단계:

    • 읽기/쓰기는 메인 메모리 (가시성 O)
    • RMW 전체는 비원자
  2. 적합:

    • 단순 쓰기 (플래그)
    • 참조 교체
  3. 부적합:

    • count++ (Atomic)
    • 복합 연산

7️⃣ volatile vs synchronized

7.1 비교

항목volatilesynchronized
가시성OO
원자성XO
상호 배제XO
없음모니터 락
성능빠름느림 (락)
블로킹XO (BLOCKED)
용도단순 플래그복합 연산

7.2 한 문장 차이

volatile vs synchronized 한 문장:

  volatile 은 가시성만 보장하고 (락 없음, 빠름),
  synchronized 는 가시성 + 원자성 + 상호 배제를 보장한다 (락, 느림).

선택:
  - 단순 플래그/참조: volatile
  - 복합 연산: synchronized

7.3 volatile 의 장점

volatile 의 장점:

  - 락 없음 (블로킹 X)
  - 빠름
  - 데드락 위험 X
  - 단순 가시성에 적합

한계:
  - 원자성 X
  - 상호 배제 X

7.4 synchronized 의 장점

synchronized 의 장점:

  - 가시성 + 원자성
  - 상호 배제
  - 복합 연산 안전

비용:
  - 락 (BLOCKED)
  - 성능
  - 데드락 위험

7.5 선택 가이드

선택 가이드:

volatile:
  ✓ boolean 플래그 (stop, ready)
  ✓ 단순 상태
  ✓ 참조 교체 (불변 객체)

synchronized:
  ✓ 복합 연산 (count++)
  ✓ 여러 변수 일관성
  ✓ check-then-act

Atomic:
  ✓ 단일 변수 원자 연산
  ✓ count++ (lock-free)

동시성 컬렉션:
  ✓ Map, List, Queue

7.6 ILIC 의 맥락

@Service
public class VolatileVsSynchronized {
    
    // volatile — 플래그 (가시성)
    private volatile boolean active = true;
    
    public void deactivate() {
        active = false;   // 락 없이 가시성
    }
    
    // synchronized — 복합 연산 (원자성)
    private int balance = 0;
    private final List<Transaction> history = new ArrayList<>();
    
    public synchronized void transfer(int amount) {
        balance += amount;                       // 원자성 + 가시성
        history.add(new Transaction(amount));    // 일관성
    }
    
    // Atomic — 단일 카운터 (lock-free)
    private final AtomicInteger counter = new AtomicInteger();
    
    public void count() {
        counter.incrementAndGet();   // 원자적, 빠름
    }
    
    record Transaction(int amount) {}
}

7.7 자기 점검 답변

volatile vs synchronized 차이는?

:
1. 한 문장:

  • volatile: 가시성만 (락 없음)
  • synchronized: 가시성+원자성+상호배제 (락)
  1. volatile:

    • 빠름, 블로킹 X
    • 원자성 X
  2. synchronized:

    • 원자성, 상호 배제
    • 락 (느림)
  3. 선택:

    • 플래그: volatile
    • 복합: synchronized/Atomic

8️⃣ happens-before와 적절한 사용

8.1 happens-before

happens-before:

  Java Memory Model (JMM) 의 규칙.
  A happens-before B 면:
    - A 의 결과가 B 에 보임
    - 순서 보장

규칙:
  - 프로그램 순서
  - volatile 쓰기 → 읽기
  - 락 해제 → 획득
  - Thread.start → run
  - run 종료 → join

8.2 volatile happens-before

volatile happens-before:

  volatile 변수 쓰기
    happens-before
  이후 그 변수 읽기

효과:
  - 쓰기 전 모든 작업이
  - 읽기 후 보임

예:
  x = 42;          // (1)
  volatileFlag = true;   // (2) volatile 쓰기
  // 다른 스레드:
  if (volatileFlag)      // (3) volatile 읽기
      print(x);          // (4): 42 보장 (happens-before)

8.3 이중 검사 잠금

// Double-Checked Locking (volatile 필수)
public class Singleton {
    private static volatile Singleton instance;   // ★ volatile
    
    public static Singleton getInstance() {
        if (instance == null) {              // 1차 (락 없이)
            synchronized (Singleton.class) {
                if (instance == null) {      // 2차 (락 안)
                    instance = new Singleton();   // volatile 쓰기
                }
            }
        }
        return instance;
    }
    // volatile 없으면:
    // - 부분 초기화 객체 노출 가능
    // - 재정렬 문제
}

8.4 적절한 사용 사례

// 1. 종료 플래그
private volatile boolean shutdown = false;

// 2. 일회성 발행 (불변 객체)
private volatile Config config;   // 참조 교체

// 3. 독립적 관찰 (한 쓰기, 여러 읽기)
private volatile long lastUpdateTime;

// 4. DCL 싱글톤
private static volatile Instance instance;

// 5. 상태 플래그
private volatile State currentState;

8.5 부적절한 사용

// ❌ 카운터
private volatile int count;   // count++ 위험

// ❌ 여러 변수 일관성
private volatile int x, y;   // x, y 함께 변경 시 불일치

// ❌ check-then-act
private volatile boolean initialized;
if (!initialized) {   // 확인
    initialized = true;   // 행동 (사이 개입)
    init();
}

8.6 ILIC 의 맥락

@Service
public class VolatileBestPractices {
    
    // ✓ 종료 플래그
    private volatile boolean running = true;
    
    // ✓ 설정 참조 (불변 객체 교체)
    private volatile ShipmentConfig config = ShipmentConfig.defaults();
    
    // ✓ 마지막 갱신 시각 (독립 관찰)
    private volatile long lastProcessedTime;
    
    public void process(Shipment shipment) {
        while (running) {   // 가시성
            doProcess(shipment);
            lastProcessedTime = System.currentTimeMillis();   // 단일 쓰기
        }
    }
    
    public void reloadConfig(ShipmentConfig newConfig) {
        config = newConfig;   // 원자적 참조 교체 + happens-before
        // config 의 모든 필드가 다른 스레드에 보임
    }
    
    public void stop() {
        running = false;   // 즉시 보임
    }
    
    private void doProcess(Shipment s) { }
}

8.7 자기 점검 답변

happens-before와 적절한 사용은?

:
1. happens-before:

  • JMM 규칙
  • A 결과가 B 에 보임
  1. volatile:

    • 쓰기 happens-before 읽기
    • 쓰기 전 작업 보임
  2. 적절:

    • 플래그, 참조 교체
    • DCL, 상태
  3. 부적절:

    • 카운터, 복합 연산
    • check-then-act

9️⃣ 면접 + 자기 점검 + 마스터 50문항 + Phase 4 완주

9.1 면접 단골 질문 매핑

Q핵심 답변
가시성 문제?변경이 안 보임 (캐시)
runFlag 무한 루프?캐시 옛 값
CPU 캐시?코어별, 불일치
volatile 효과?메인 메모리 직접 (가시성)
volatile 원자성?보장 X
count++ volatile?위험 (RMW)
volatile vs sync?가시성 vs 가시성+원자성
happens-before?결과 보임 보장
DCL volatile?부분 초기화 방지
volatile 적합?플래그, 참조

9.2 마스터 자기 점검 체크리스트

가시성

  • 정의
  • 원인 (캐시)
  • runFlag 시나리오

CPU 캐시

  • 계층
  • 코어별
  • 메모리 배리어

volatile

  • 효과 (가시성, 순서)
  • 메인 메모리 직접
  • 재정렬 방지

원자성 X

  • count++ 위험
  • 가시성 ≠ 원자성

적합/부적합

  • 플래그, 참조
  • 카운터 부적합

vs synchronized

  • 비교
  • 선택

happens-before

  • JMM
  • DCL

9.3 volatile 마스터 50문항

가시성 (12문항)

Q1. 가시성 문제? → 변경 안 보임
Q2. 원인? → CPU 캐시
Q3. runFlag? → 무한 루프
Q4. 캐시 옛 값? → 변경 미반영
Q5. 컴파일러 최적화? → 레지스터 캐싱
Q6. 비결정성? → 환경마다
Q7. 메인 메모리? → 공유
Q8. 코어 캐시? → 개별
Q9. 가시성 vs 원자성? → 보임 vs 분리
Q10. MESI? → 캐시 일관성
Q11. 메모리 배리어? → 동기화
Q12. 해결? → volatile

volatile 효과 (13문항)

Q13. volatile? → 메인 메모리 직접
Q14. 효과 1? → 가시성
Q15. 효과 2? → 순서 (재정렬 방지)
Q16. 쓰기? → flush
Q17. 읽기? → 최신
Q18. 캐시? → 우회
Q19. 비용? → 약간 느림
Q20. 락? → 없음
Q21. 블로킹? → X
Q22. 재정렬? → 방지
Q23. happens-before? → 쓰기→읽기
Q24. 단순 쓰기? → 안전
Q25. 단순 읽기? → 안전

원자성 X (12문항)

Q26. volatile 원자성? → 보장 X
Q27. count++ volatile? → 위험
Q28. 이유? → RMW 3단계
Q29. 가시성 O? → 각 읽기/쓰기 메인
Q30. 원자성 X? → 전체 비원자
Q31. 손실? → 둘 다 읽고 덮어씀
Q32. 단일 쓰기? → 안전 (flag=true)
Q33. += 위험? → RMW
Q34. 카운터? → Atomic
Q35. 복합 연산? → synchronized
Q36. check-then-act? → 위험
Q37. long/double? → volatile 로 원자적 (64비트)

vs synchronized + 사용 (13문항)

Q38. volatile vs sync? → 가시성 vs +원자성
Q39. volatile 장점? → 빠름, 락 X
Q40. sync 장점? → 원자성, 상호 배제
Q41. volatile 적합? → 플래그
Q42. sync 적합? → 복합
Q43. Atomic? → 단일 변수 원자
Q44. happens-before 규칙? → JMM
Q45. volatile happens-before? → 쓰기→읽기
Q46. DCL? → volatile 필수
Q47. 부분 초기화? → volatile 방지
Q48. 참조 교체? → volatile 적합
Q49. 종료 플래그? → volatile
Q50. 여러 변수? → synchronized

9.4 채점

50 / 50 → volatile 마스터
45-49   → 거의 마스터
40-44   → 복습
< 40    → Unit 4.5 재학습

9.5 Phase 4 완주 정리

Phase 4 — synchronized & volatile (★ 1차 정점)

Unit 4.1 — 임계 영역과 동기화의 필요성
  - 임계 영역, count++ 3단계
  - 경쟁 조건, 원자성

Unit 4.2 — synchronized 메서드
  - this 락, Class 락
  - 한 번에 하나

Unit 4.3 — synchronized 블록
  - 범위 최소화
  - 잠금 대상 명시

Unit 4.4 — 모니터 락 (★ 마스터)
  - intrinsic lock
  - BLOCKED, 공정성 X
  - synchronized 단점

Unit 4.5 — volatile (★ 마스터)
  - 가시성
  - 원자성 X
  - vs synchronized

9.6 Phase 4 핵심 통찰

Phase 4 핵심 통찰 5가지:

1. count++ 는 3단계 (비원자)
   - 경쟁 조건, 손실

2. synchronized 로 원자성
   - this/Class 락
   - 한 번에 하나

3. 모니터 락
   - 모든 객체
   - 공정성 X, 단점 3가지

4. volatile 은 가시성만
   - 원자성 X
   - count++ 위험

5. 도구 선택
   - 플래그: volatile
   - 복합: synchronized/Atomic

9.7 4주차 누적 진행

✅ Phase 1 — 동시성의 기초 (4 Unit)
✅ Phase 2 — 4분면 매트릭스 (3 Unit)
✅ Phase 3 — 스레드 다루기 (5 Unit)
✅ Phase 4 — synchronized & volatile (5 Unit) ★ 1차 정점 ← 완주
⏭ Phase 5 — Lock 도구 (4 Unit)
⏭ Phase 6 — 스레드 협력 (4 Unit)
⏭ Phase 7 — Executor (7 Unit) ★ 2차 정점
⏭ Phase 8 — 고급 비동기 (3 Unit)

총: 17/35 Unit (Phase 4 완주, 약 49%)

9.8 추가 심화 질문

Q1: volatile 배열?

답:

  • volatile int[] arr
  • arr 참조는 volatile (가시성)
  • arr[i] 요소는 volatile X
  • 요소 변경은 가시성 보장 안 됨
  • AtomicIntegerArray 사용

Q2: final 과 가시성?

답:

  • final 필드는 생성자 후 가시성 보장
  • 안전한 발행 (safe publication)
  • 불변 객체 스레드 안전 근거

Q3: volatile 과 성능?

답:

  • 일반 변수보다 느림 (캐시 우회)
  • synchronized 보다 빠름 (락 없음)
  • 읽기 많으면 비용 적음
  • 쓰기 시 flush 비용

Q4: AtomicInteger 와 volatile?

답:

  • AtomicInteger 내부에 volatile int value
    • CAS 연산
  • 가시성 (volatile) + 원자성 (CAS)
  • volatile 단독보다 강력

Q5: 64비트 변수와 volatile?

답:

  • long/double 은 32비트 JVM 에서 비원자
  • volatile 붙이면 64비트 원자적 쓰기
  • word tearing 방지
  • 가시성 + 원자성 (단일 쓰기)

🎯 핵심 요약 — 3줄 정리

1. 가시성과 volatile

  • 가시성 문제: 변경 안 보임 (캐시)
  • volatile: 메인 메모리 직접 (가시성 보장)

2. 원자성 X

  • volatile 은 가시성만
  • count++ 는 여전히 위험 (RMW)

3. 선택

  • 플래그/참조: volatile
  • 복합 연산: synchronized / Atomic
  • volatile vs synchronized: 가시성 vs 가시성+원자성

🏆 Phase 4 완주 — synchronized & volatile 마스터 (★ 1차 정점)

🚀 Phase 4 — synchronized & volatile (★ 1차 정점)
  ✅ Unit 4.1 임계 영역과 동기화의 필요성
  ✅ Unit 4.2 synchronized 메서드
  ✅ Unit 4.3 synchronized 블록
  ✅ Unit 4.4 모니터 락 (★ 마스터)
  ✅ Unit 4.5 volatile (★ 마스터) ← 여기, Phase 4 완주

→ 임계 영역 + 원자성
→ synchronized (this/Class/모니터 락)
→ volatile (가시성)
→ ★ 4주차 1차 정점 완료

📚 다음으로...

Phase 5 — 정교한 락: LockSupport와 ReentrantLock

Phase 5 — Lock 도구 (4 Unit)

Unit 5.1 — synchronized의 한계 정리
Unit 5.2 — LockSupport (저수준 도구)
Unit 5.3 — ReentrantLock (실무 표준)
Unit 5.4 — tryLock으로 데드락 회피 (★ 마스터)

4주차 누적 진행

✅ Phase 1 — 동시성의 기초 (4 Unit)
✅ Phase 2 — 4분면 매트릭스 (3 Unit)
✅ Phase 3 — 스레드 다루기 (5 Unit)
✅ Phase 4 — synchronized & volatile (5 Unit) ★ 1차 정점
⏭ Phase 5 — Lock 도구 (4 Unit)

총: 17/35 Unit (약 49%)

★ 마스터 Unit — volatile 완료
🏆 Phase 4 완주 — synchronized & volatile 마스터 (★ 1차 정점 완료)

profile
Software Developer

0개의 댓글