F-LAB JAVA · 5주차 · Phase 2 · 동시성 안전 도구 3종 비교
🚀 Phase 2 시작 — 가시성과 원자성
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
동시성 문제는 정확히 가시성 (Visibility) 과 원자성 (Atomicity) 두 가지이며, 가시성은 "한 스레드의 변경이 다른 스레드에 보이지 않는 문제", 원자성은 "동시 수정이 서로를 덮어쓰는 문제" 로, 이 둘을 분리해서 봐야 도구를 정확히 선택할 수 있다.
가시성 문제 는 CPU 캐시 때문에 한 스레드가 변경한 값 (예:runFlag = false) 이 다른 스레드의 캐시에 반영되지 않아 발생하며, 대표 증상은 무한 루프다.
원자성 문제 는count++같은 복합 연산 (읽기-수정-쓰기) 이 중간에 다른 스레드의 개입으로 분리되어 일부 갱신이 손실되는 (lost update) 문제다.
이 둘을 분리해서 보는 것이 핵심인 이유는 도구마다 해결 범위가 다르기 때문이다 —volatile은 가시성만,synchronized·Atomic은 가시성과 원자성을 모두 해결한다.
count++는 가시성과 원자성을 둘 다 깨는 대표 사례이고, 단순 플래그 (boolean) 는 가시성만 문제가 되는 사례다.
동시성 문제 = 공유 화이트보드:
가시성 문제 (안 보임):
- A 가 보드에 "회의 취소" 적음
- 하지만 B 는 자기 수첩만 봄 (캐시)
- B 는 "회의 있음" 으로 착각
→ 변경이 안 보임
원자성 문제 (덮어씀):
- 보드에 "참석자: 5명"
- A 와 B 가 동시에 +1 하려 함
- A: 5 읽음 → 6 적음
- B: 5 읽음 (A 와 동시) → 6 적음
- 결과 6 (7 이어야 하는데!)
→ 동시 수정이 덮어씀
분리해서 보기:
- 둘은 다른 문제
- 해결책도 다름
- 가시성: 항상 보드 직접 확인 (volatile)
- 원자성: 한 명만 수정 (synchronized/Atomic)
→ 동시성 문제 = 가시성 (안 보임) + 원자성 (덮어씀), 분리해야 도구 선택 정확.
1. 동시성 문제는 2가지
2. 가시성 문제
3. 원자성 문제
4. 분리해서 보기의 중요성
5. 가시성만 깨지는 경우
6. 원자성만 깨지는 경우
7. 둘 다 깨지는 경우 (count++)
8. 도구별 해결 범위
9. 면접 + 자기 점검
동시성 문제 = 정확히 2가지:
1. 가시성 (Visibility)
- 변경이 안 보임
2. 원자성 (Atomicity)
- 동시 수정 덮어씀
이 둘로 분류
| 문제 | 정의 | 증상 |
|---|---|---|
| 가시성 | 변경이 안 보임 | 무한 루프 |
| 원자성 | 동시 수정 덮어씀 | 값 손실 |
2가지로 보는 이유:
- 원인 다름
- 가시성: 캐시
- 원자성: 연산 분리
- 해결 다름
- 도구마다 범위 다름
→ 정확한 진단 + 처방
@Service
public class TwoConcurrencyProblems {
// 가시성 문제 후보 — 플래그
private boolean running = true; // 가시성 (캐시)
// 원자성 문제 후보 — 카운터
private int processedCount = 0; // 원자성 (count++)
public void worker() {
while (running) { // 가시성: running 변경 안 보일 수 있음
processedCount++; // 원자성: 동시 증가 손실
}
}
public void stop() {
running = false; // 다른 스레드에 안 보일 수 있음 (가시성)
}
// 두 문제가 별개로 존재
}
동시성 문제가 정확히 2가지인 이유는?
답:
1. 2가지:
표:
이유:
목적:
가시성 (Visibility) 문제:
한 스레드의 변경이
다른 스레드에 보이지 않음.
원인:
- CPU 캐시 (코어별)
- 변경이 캐시에만
// 가시성 문제 — 무한 루프
private boolean runFlag = true;
// 스레드 1
while (runFlag) { // 캐시의 true 만 봄
work();
}
// runFlag = false 변경을 못 볼 수 있음 → 무한 루프
// 스레드 2
runFlag = false; // 변경 (스레드 1 이 못 봄)
가시성 원인 — CPU 캐시:
각 코어 자기 캐시:
- 스레드 1: 코어 1 캐시 (true)
- 스레드 2: 코어 2 에서 false 변경
- 코어 1 캐시 미반영
- 스레드 1 은 true 계속
가시성 해결:
- volatile (메인 메모리 직접)
- synchronized (동기화)
- Atomic (내부 volatile)
→ 가시성 보장
@Service
public class VisibilityProblem {
// ❌ 가시성 문제
private boolean shutdownRequested = false;
public void worker() {
while (!shutdownRequested) { // 변경 안 보일 수 있음
processNext();
}
}
public void requestShutdown() {
shutdownRequested = true; // worker 가 못 볼 수 있음
}
// ✓ 해결 — volatile
private volatile boolean shutdownSafe = false;
public void workerSafe() {
while (!shutdownSafe) { // 항상 메인 메모리 (보임)
processNext();
}
}
private void processNext() { }
}
가시성 문제의 정의는?
답:
1. 정의:
증상:
원인:
해결:
원자성 (Atomicity) 문제:
동시 수정이 서로를 덮어씀.
- 복합 연산 분리
- 일부 갱신 손실 (lost update)
// 원자성 문제 — count++ 손실
private int count = 0;
// 여러 스레드가 동시에
count++; // read → +1 → write (3단계)
// 기대: N번 증가 = N
// 실제: < N (손실)
원자성 원인 — 연산 분리:
count++ 는 3단계:
1. count 읽기
2. +1
3. count 쓰기
두 스레드:
- 둘 다 같은 값 읽음
- 둘 다 +1
- 하나 덮어씀 (손실)
원자성 문제:
스레드 A 스레드 B count
읽기(0) 0
읽기(0) 0 ← 둘 다 0
+1 (1)
+1 (1)
쓰기(1) 1
쓰기(1) 1 ← A의 1 덮어씀
2번 증가했는데 1 (손실)
원자성 해결:
- synchronized (한 스레드만)
- Atomic (CAS)
- Lock
volatile 은 X (원자성 보장 안 됨)
@Service
public class AtomicityProblem {
// ❌ 원자성 문제
private int processedCount = 0;
public void process(Shipment shipment) {
doProcess(shipment);
processedCount++; // 동시 증가 손실
}
// ✓ 해결 — AtomicInteger
private final AtomicInteger safeCount = new AtomicInteger();
public void processSafe(Shipment shipment) {
doProcess(shipment);
safeCount.incrementAndGet(); // 원자적 (손실 X)
}
private void doProcess(Shipment s) { }
}
원자성/동시 접근 문제의 정의는?
답:
1. 정의:
증상:
원인:
해결:
분리해서 보는 이유:
도구마다 해결 범위 다름:
- volatile: 가시성만
- synchronized: 둘 다
- Atomic: 둘 다
→ 문제 진단 → 도구 선택
잘못된 선택 방지:
분리 안 하면:
- 원자성 문제에 volatile (틀림)
- count++ 에 volatile (여전히 손실)
분리하면:
- 가시성만 → volatile
- 원자성 → Atomic/synchronized
진단 흐름:
문제 발생
↓
가시성? (변경 안 보임)
→ volatile / synchronized
↓
원자성? (값 손실)
→ Atomic / synchronized
↓
둘 다? (count++)
→ Atomic / synchronized
| 문제 | 적합 도구 |
|---|---|
| 가시성만 | volatile |
| 원자성만 | (드묾) Atomic |
| 둘 다 | Atomic, synchronized |
@Service
public class SeparateAnalysis {
// 가시성만 → volatile
private volatile boolean running = true;
public void stop() { running = false; } // 단일 쓰기
// 원자성 (+가시성) → Atomic
private final AtomicInteger count = new AtomicInteger();
public void increment() { count.incrementAndGet(); } // 복합
// 복잡한 복합 연산 → synchronized
private int balance = 0;
private final List<String> history = new ArrayList<>();
public synchronized void transfer(int amount) {
balance += amount; // 여러 변수 일관성
history.add("transfer: " + amount);
}
// 진단: 가시성? 원자성? 복합?
// → 적절한 도구 선택
}
두 문제를 분리해서 보는 것이 왜 중요한가?
답:
1. 이유:
잘못된 선택 방지:
진단 흐름:
선택:
가시성만 깨지는 경우:
단순 boolean 플래그:
- 한 스레드만 쓰기
- 여러 스레드 읽기
단일 쓰기 = 원자성 OK
but 가시성 X
// 가시성만 문제 (원자성 OK)
private boolean shutdown = false;
// 스레드 1 (유일한 쓰기)
shutdown = true; // 단일 쓰기 (원자적)
// 스레드 2~N (읽기만)
while (!shutdown) { // 가시성 X (안 보일 수 있음)
work();
}
// 단일 쓰기라 원자성 OK
// 하지만 가시성 X → volatile 필요
왜 원자성 OK:
boolean 단일 쓰기:
- true 또는 false
- 중간 상태 없음
- 한 번에 (원자적)
복합 연산 아님:
- count++ 같은 게 아님
- 단순 대입
// volatile 로 가시성 해결
private volatile boolean shutdown = false;
shutdown = true; // 단일 쓰기 + 가시성
// 원자성 (단일 쓰기) + 가시성 (volatile)
// → volatile 만으로 충분
@Service
public class VisibilityOnly {
// 가시성만 문제 → volatile 충분
private volatile boolean active = true;
private volatile String currentStatus = "IDLE";
private volatile long lastUpdateTime;
// 모두 단일 쓰기 (원자성 OK) + 가시성 필요
public void deactivate() {
active = false; // 단일 쓰기 (volatile)
}
public void updateStatus(String status) {
currentStatus = status; // 단일 쓰기
lastUpdateTime = System.currentTimeMillis(); // 단일 쓰기
}
// 여러 스레드가 읽기 → volatile 로 가시성 보장
}
가시성만 깨지는 경우는?
답:
1. 경우:
원자성 OK:
가시성 X:
해결:
원자성만 깨지는 경우:
실제로는 드묾:
- 원자성 깨지면
- 보통 가시성도 관련
이론적:
- synchronized 안에서 가시성 보장
- 하지만 동기화 부족 시 원자성
단일 변수 원자성:
long/double 쓰기:
- 32비트 JVM 에서 비원자 (word tearing)
- 64비트를 32비트씩 쓰기
volatile long:
- 원자적 쓰기 보장
- + 가시성
// long 쓰기 (32비트 JVM 에서 비원자)
private long value; // 64비트
value = 0x1234567890ABCDEFL;
// 32비트 JVM: 상위 32 + 하위 32 (2번)
// 다른 스레드가 중간에 읽으면 깨진 값
// volatile 로 원자적
private volatile long valueSafe;
실무에서:
원자성 문제 = 보통 가시성도
- count++ : 둘 다
- 복합 연산: 둘 다
순수 원자성만:
- long/double word tearing (드묾)
→ 보통 함께 고려
@Service
public class AtomicityOnly {
// long word tearing (32비트 JVM, 드묾)
private volatile long totalWeight; // volatile 로 원자적 쓰기
// 대부분: 원자성 = 가시성도 (count++)
private final AtomicLong count = new AtomicLong();
public void addWeight(long weight) {
// long 누적 (원자성 + 가시성 필요)
// AtomicLong 으로 둘 다
count.addAndGet(weight);
}
// 순수 원자성만 깨지는 경우는 드묾
// 보통 가시성과 함께
}
원자성만 깨지는 경우는?
답:
1. 드묾:
단일 변수:
word tearing:
실무:
count++ — 둘 다 깨짐:
가시성:
- count 변경 안 보일 수 있음
원자성:
- read-modify-write 분리
- 손실
private int count = 0;
count++;
// 1. read count (가시성: 옛 값 읽을 수 있음)
// 2. +1
// 3. write count (가시성: 안 보일 수 있음 + 원자성: 덮어씀)
// 가시성 + 원자성 둘 다
// volatile 로 가시성 OK, 원자성 X
private volatile int count = 0;
count++;
// 가시성: O (메인 메모리)
// 원자성: X (여전히 3단계 분리)
// → 손실
// → volatile 부족
// → Atomic 또는 synchronized
// ✓ AtomicInteger (둘 다)
private final AtomicInteger count = new AtomicInteger();
count.incrementAndGet(); // 가시성 + 원자성
// ✓ synchronized (둘 다)
private int count = 0;
public synchronized void inc() { count++; }
복합 연산 일반:
count++ 외에도:
- count += n
- count = count * 2
- if (x == 0) x = 1 (check-then-act)
- list.add (확인 + 추가)
모두 둘 다 문제
→ Atomic / synchronized
@Service
public class BothProblems {
// ❌ count++ (둘 다 문제)
private int wrongCount = 0;
public void wrong() { wrongCount++; }
// ❌ volatile 도 부족 (원자성 X)
private volatile int volatileCount = 0;
public void stillWrong() { volatileCount++; } // 손실
// ✓ AtomicInteger (둘 다 해결)
private final AtomicInteger correctCount = new AtomicInteger();
public void correct() { correctCount.incrementAndGet(); }
// ✓ synchronized (둘 다, 복잡한 경우)
private int balance = 0;
public synchronized void transfer(int amount) {
balance += amount; // 복합 + 일관성
}
}
둘 다 깨지는 경우는?
답:
1. count++:
volatile 부족:
해결:
복합 연산:
| 도구 | 가시성 | 원자성 | 방식 |
|---|---|---|---|
| synchronized | ✅ | ✅ | 락 (블로킹) |
| volatile | ✅ | ❌ | 메모리 동기화 |
| Atomic | ✅ | ✅ | CAS (논블로킹) |
volatile:
✅ 가시성
❌ 원자성
적합:
- 단순 플래그
- 단일 쓰기
synchronized:
✅ 가시성
✅ 원자성
- 락 (느림)
적합:
- 복잡한 복합 연산
- 여러 변수
Atomic:
✅ 가시성
✅ 원자성
- CAS (논블로킹, 빠름)
적합:
- 단일 변수 원자 연산
- count++
선택 가이드:
가시성만 (플래그):
→ volatile
원자성 (단일 변수):
→ Atomic
복잡한 복합 (여러 변수):
→ synchronized
→ 문제 진단 후 선택
@Service
public class ToolScopeGuide {
// 가시성만 → volatile
private volatile boolean running = true;
// 원자성 (단일) → Atomic
private final AtomicInteger count = new AtomicInteger();
// 복잡한 복합 → synchronized
private int balance = 0;
private final List<String> log = new ArrayList<>();
public void stop() { running = false; } // volatile
public void increment() { count.incrementAndGet(); } // Atomic
public synchronized void transfer(int amount) { // synchronized
balance += amount;
log.add("transfer");
}
// 문제 유형 → 도구
// (Phase 2 의 나머지 Unit 에서 상세)
}
도구마다 해결 범위가 다른 이유는?
답:
1. 3종:
volatile:
Atomic:
선택:
| Q | 핵심 답변 |
|---|---|
| 동시성 문제 2가지? | 가시성, 원자성 |
| 가시성? | 변경 안 보임 |
| 원자성? | 동시 수정 덮어씀 |
| 분리 이유? | 도구 범위 다름 |
| 가시성만? | 단순 플래그 |
| 원자성만? | long word tearing (드묾) |
| 둘 다? | count++ |
| volatile 범위? | 가시성만 |
| synchronized 범위? | 둘 다 |
| Atomic 범위? | 둘 다 (CAS) |
답:
답:
답:
답:
답:
1. 두 가지 문제
2. 분리해서 보기
3. 사례
이번 Unit에서 두 문제를 구분했다면, 다음은 synchronized (둘 다 해결).
🚀 Phase 2 — 동시성 안전 도구 3종 비교
✅ Unit 2.1 두 가지 동시성 문제 구분 ← 여기
⏭ Unit 2.2 synchronized
⏭ Unit 2.3 volatile
⏭ Unit 2.4 Atomic + CAS (★ 마스터)
✅ Phase 1 — 스레드 풀 필요성 (3 Unit)
🚀 Phase 2 — 동시성 안전 도구 (1/4 진행)
총: 4/26 Unit