F-LAB JAVA · 5주차 · Phase 2 · 동시성 안전 도구 3종 비교
★ 마스터 Unit — 면접 핵심 (★★★) + Phase 2 완주 (Part A 동시성 완성)
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
CAS (Compare And Swap) 는 "현재 값을 읽고, 새 값을 계산한 뒤, 메모리의 값이 읽었던 값과 같으면 교체하고 다르면 재시도" 하는 알고리즘으로, Atomic 클래스는 이를 통해 락 없이 (논블로킹) 원자성을 보장한다.
CAS 4단계 — (1) 현재 값을 읽어 레지스터에 저장 (기대값 A), (2) 새 값 계산 (B), (3) 메모리의 값과 A 를 비교, (4) 같으면 B 로 교체 (성공), 다르면 다른 스레드가 이미 수정한 것이므로 재시도한다.
논블로킹 — synchronized 는 락을 잡고 다른 스레드를 BLOCKED 시키지만, CAS 는 락 없이 단일 CPU 명령 (compareAndSwap) 으로 시도하고 실패하면 재시도하므로 스레드가 대기하지 않는다.
3종 비교 — synchronized (가시성 ✅, 원자성 ✅, 락, 가장 느림), volatile (가시성 ✅, 원자성 ❌, 메모리 동기화, 빠름), Atomic (가시성 ✅, 원자성 ✅, CAS, 빠름) 이다.
ABA 문제 — 값이 A→B→A 로 바뀌면 CAS 는 변하지 않은 것으로 오인할 수 있으며, AtomicStampedReference (버전 태그) 로 해결한다.
CAS = 공유 메모장 수정 (낙관적):
CAS 4단계:
1. 현재 값 읽기: 메모장 보니 "100" (기대값 A=100)
2. 새 값 계산: 100 + 50 = 150 (B)
3. 비교: 지금도 "100" 인가?
4-a. 같으면: "150" 으로 교체 (성공!)
4-b. 다르면 (누가 바꿈): 다시 1번부터 (재시도)
은행 잔액 예시:
스레드1: 100 읽음 → 150 시도 → 메모리 100 일치 → 성공 (150)
스레드2: 100 읽음 → 150 시도 → 메모리 이미 150 → 실패
→ 재시도: 150 읽음 → 200 시도 → 성공 (200)
논블로킹 (대기 X):
synchronized: 문 잠그고 (다른 사람 대기)
CAS: 문 안 잠그고 시도, 충돌 시 다시 (대기 X)
ABA 문제:
- 100 → 50 → 100 (왔다갔다)
- "100 그대로네?" 착각
- 버전 태그로 해결 (100#1 vs 100#3)
→ CAS = 읽기→계산→비교→교체/재시도 (낙관적, 논블로킹), Atomic 의 원리.
1. CAS 알고리즘 4단계
2. 락 없이 원자성 (논블로킹)
3. 재시도 동작
4. 은행 잔액 예시
5. 3종 비교 매트릭스
6. ABA 문제
7. Atomic이 빠른 이유
8. 주요 Atomic 클래스
9. 면접 + 자기 점검 + 마스터 50문항
CAS (Compare And Swap):
현재 값과 기대값을 비교하여
같으면 새 값으로 교체하는 원자적 연산.
- 단일 CPU 명령
- 락 없음
CAS 4단계:
1. 현재 값 읽기 → 레지스터 (기대값 A)
2. 새 값 계산 (B)
3. 메모리 값 == A 비교
4. 같으면 → B 로 교체 (성공)
다르면 → 재시도
// CAS 메서드
boolean compareAndSet(int expectedValue, int newValue);
// 동작:
// 현재 == expectedValue 면
// → newValue 로 교체, true 반환
// 다르면
// → 교체 X, false 반환 (재시도)
// AtomicInteger.incrementAndGet (개념)
public int incrementAndGet() {
int current, next;
do {
current = get(); // 1. 현재 값 (A)
next = current + 1; // 2. 새 값 (B)
} while (!compareAndSet(current, next)); // 3,4. 비교+교체 (실패 시 재시도)
return next;
}
하드웨어 지원:
CAS = CPU 명령:
- x86: CMPXCHG
- 단일 명령 (원자적)
- 락 없이 보장
JVM 이 native 로 사용
@Service
public class CASBasics {
private final AtomicInteger processedCount = new AtomicInteger(0);
// CAS 기반 증가
public void process(Shipment shipment) {
doProcess(shipment);
processedCount.incrementAndGet(); // CAS (락 없이 원자적)
}
// compareAndSet 직접 사용
private final AtomicReference<Status> status =
new AtomicReference<>(Status.IDLE);
public boolean startProcessing() {
// IDLE → PROCESSING (CAS)
return status.compareAndSet(Status.IDLE, Status.PROCESSING);
// IDLE 이면 PROCESSING 으로 (성공)
// 아니면 false (이미 처리 중)
}
enum Status { IDLE, PROCESSING, DONE }
private void doProcess(Shipment s) { }
}
CAS 알고리즘 4단계는?
답:
1. 4단계:
compareAndSet:
하드웨어:
원자적:
논블로킹 (Non-blocking):
락 없이 원자성:
- CAS 로 시도
- 실패 시 재시도
- 대기 (BLOCKED) X
블로킹 vs 논블로킹:
synchronized (블로킹):
- 락 획득
- 다른 스레드 BLOCKED
- 대기
CAS (논블로킹):
- 락 없이 시도
- 실패 시 재시도
- 대기 X (계속 진행)
낙관적 vs 비관적:
synchronized (비관적):
- "충돌할 거야" 가정
- 미리 락
CAS (낙관적):
- "충돌 안 할 거야" 가정
- 일단 시도
- 충돌 시만 재시도
논블로킹 장점:
- 대기 없음 (BLOCKED X)
- 데드락 없음 (락 X)
- 컨텍스트 스위칭 적음
- 경쟁 낮으면 빠름
논블로킹 단점:
- 경쟁 높으면 재시도 ↑
- CPU 사용 (재시도 루프)
- 복잡한 연산 어려움 (단일 변수)
- ABA 문제
@Service
public class NonBlockingAtomicity {
// 논블로킹 카운터
private final AtomicLong totalProcessed = new AtomicLong();
public void process(Shipment shipment) {
doProcess(shipment);
totalProcessed.incrementAndGet(); // 논블로킹
// 락 없이, 대기 없이 (CAS)
}
// 논블로킹 상태 전이
private final AtomicReference<ProcessState> state =
new AtomicReference<>(ProcessState.READY);
public boolean tryStart() {
// CAS 로 상태 전이 (락 없이)
return state.compareAndSet(ProcessState.READY, ProcessState.RUNNING);
// 성공: 시작, 실패: 이미 실행 중 (대기 X)
}
enum ProcessState { READY, RUNNING, DONE }
private void doProcess(Shipment s) { }
}
Atomic이 락 없이 원자성을 보장하는 원리는?
답:
1. 논블로킹:
vs 블로킹:
낙관적:
장점:
재시도 (retry):
CAS 실패 시:
- 다른 스레드가 이미 수정
- 새 값 다시 읽기
- 다시 계산
- 다시 CAS
// 재시도 루프 (스핀)
do {
current = get(); // 다시 읽기
next = compute(current); // 다시 계산
} while (!compareAndSet(current, next)); // 성공까지 재시도
CAS 실패 (재시도):
현재 값 != 기대값:
- 다른 스레드가 수정
- 기대값 안 맞음
- 재시도
→ 충돌 시 재시도
무한 반복 가능성:
이론적:
- 계속 충돌하면 무한 재시도
실제:
- 결국 성공 (확률적)
- 경쟁 높아도 진전
- lock-free 보장 (누군가 성공)
→ 라이브락 가능성 (드묾)
경쟁과 재시도:
경쟁 낮음:
- 재시도 거의 X
- 빠름
경쟁 높음:
- 재시도 ↑
- CPU 사용 ↑
- synchronized 가 나을 수도
@Service
public class RetryBehavior {
private final AtomicReference<BigDecimal> totalFreight =
new AtomicReference<>(BigDecimal.ZERO);
// CAS 재시도 (누적)
public void addFreight(BigDecimal amount) {
BigDecimal current, next;
do {
current = totalFreight.get(); // 현재 (다시 읽기)
next = current.add(amount); // 계산 (다시)
} while (!totalFreight.compareAndSet(current, next)); // 재시도
// 다른 스레드가 끼어들면 재시도
}
// updateAndGet (재시도 내장)
public BigDecimal addFreightSimple(BigDecimal amount) {
return totalFreight.updateAndGet(current -> current.add(amount));
// 내부적으로 CAS 재시도
}
}
CAS의 "재시도" 동작은?
답:
1. 재시도:
루프:
언제 실패:
무한?:
은행 잔액 예시:
초기 잔액: 100
스레드1: +50 (100 → 150)
스레드2: +50 (100 → 150) 동시
CAS 흐름:
스레드1:
1. 100 읽음 (기대값 100)
2. 100 + 50 = 150 계산
3. 메모리 100 == 100? YES
4. 150 으로 교체 (성공)
잔액: 150
스레드2 (스레드1 직후):
1. 100 읽음 (기대값 100)
2. 100 + 50 = 150 계산
3. 메모리 150 == 100? NO (변함!)
4. 재시도
1'. 150 읽음 (기대값 150)
2'. 150 + 50 = 200
3'. 메모리 150 == 150? YES
4'. 200 으로 교체 (성공)
잔액: 200 ✓
손실 없음:
최종 잔액: 200 (100 + 50 + 50)
- 스레드1: 150
- 스레드2: 재시도 → 200
CAS 가 손실 방지:
- 충돌 감지 (비교)
- 재시도 (최신 값)
@Service
public class BankBalanceExample {
private final AtomicReference<BigDecimal> balance =
new AtomicReference<>(BigDecimal.valueOf(100));
// CAS 입금
public void deposit(BigDecimal amount) {
BigDecimal current, next;
do {
current = balance.get(); // 현재 잔액
next = current.add(amount); // 새 잔액
} while (!balance.compareAndSet(current, next)); // CAS
// 충돌 시 재시도 → 손실 없음
}
// 두 스레드가 동시 +50 해도
// 최종 200 (손실 X)
}
// synchronized 버전 (블로킹)
private BigDecimal balance = BigDecimal.valueOf(100);
public synchronized void deposit(BigDecimal amount) {
balance = balance.add(amount); // 락 (한 스레드씩)
}
// CAS: 논블로킹 (재시도)
// synchronized: 블로킹 (대기)
// 결과 동일, 방식 다름
@Service
public class FreightBalanceCAS {
// 운임 정산 잔액 (CAS)
private final AtomicReference<BigDecimal> accountBalance =
new AtomicReference<>(BigDecimal.ZERO);
// 운임 청구 (차감)
public boolean charge(BigDecimal freight) {
BigDecimal current, next;
do {
current = accountBalance.get();
if (current.compareTo(freight) < 0) {
return false; // 잔액 부족
}
next = current.subtract(freight);
} while (!accountBalance.compareAndSet(current, next));
return true;
// 동시 청구해도 정확 (CAS 재시도)
}
// 충전 (가산)
public void recharge(BigDecimal amount) {
accountBalance.updateAndGet(current -> current.add(amount));
}
}
은행 잔액 예시로 CAS 흐름은?
답:
1. 시나리오:
스레드1:
스레드2:
결과:
| 도구 | 가시성 | 원자성 | 방식 | 성능 |
|---|---|---|---|---|
| synchronized | ✅ | ✅ | 락 (블로킹) | 가장 느림 |
| volatile | ✅ | ❌ | 메모리 동기화 | 빠름 |
| Atomic | ✅ | ✅ | CAS (논블로킹) | 빠름 |
가시성 (셋 다 ✅):
synchronized: 락 동기화
volatile: 메인 메모리 직접
Atomic: 내부 volatile + CAS
→ 모두 가시성 보장
원자성:
synchronized: ✅ (락)
volatile: ❌ (복합 X)
Atomic: ✅ (CAS)
→ volatile 만 원자성 X
방식:
synchronized: 블로킹 (락, 대기)
volatile: 메모리 동기화 (가시성만)
Atomic: 논블로킹 (CAS, 재시도)
선택 가이드:
가시성만 (플래그):
→ volatile
단일 변수 원자 연산:
→ Atomic (빠름)
복잡한 복합 (여러 변수):
→ synchronized
→ 용도별
@Service
public class ThreeToolsComparison {
// 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 total = 0;
private final Map<Long, Shipment> shipments = new HashMap<>();
public synchronized void addShipment(Shipment s) {
shipments.put(s.getId(), s);
total++;
// 두 상태 일관성 → synchronized
}
// 각 도구를 용도에 맞게
}
3종 비교 매트릭스는?
답:
1. 가시성:
원자성:
방식:
선택:
ABA 문제:
값이 A → B → A 로 변하면:
- CAS 는 A 그대로 봄
- 변하지 않은 것으로 오인
- 사실은 변했는데
ABA 시나리오:
스레드1: A 읽음 (기대값 A)
↓ (스레드1 잠시 멈춤)
스레드2: A → B 변경
스레드3: B → A 변경 (다시 A)
↓
스레드1: CAS (A == A? YES) → 성공
- 하지만 중간에 B 거쳐감
- 스레드1 은 모름
왜 문제:
단순 값:
- ABA 무해 (값만 같으면 OK)
참조/구조:
- 중간 변화 중요
- 예: 스택 pop/push
- 노드 재사용 시 오류
// AtomicStampedReference (버전 태그)
AtomicStampedReference<String> ref =
new AtomicStampedReference<>("A", 0); // 값 + 스탬프(버전)
int[] stampHolder = new int[1];
String value = ref.get(stampHolder); // 값 + 스탬프 읽기
int stamp = stampHolder[0];
// CAS with 스탬프
ref.compareAndSet("A", "B", stamp, stamp + 1);
// 값 + 스탬프 모두 일치해야 성공
// A → B → A 면 스탬프 다름 → 감지
ABA 해결 클래스:
AtomicStampedReference:
- 값 + int 스탬프 (버전)
- 버전으로 변화 감지
AtomicMarkableReference:
- 값 + boolean 마크
- 표시 여부
@Service
public class ABAProblem {
// 일반 AtomicReference (ABA 가능)
private final AtomicReference<Node> head = new AtomicReference<>();
// ABA 방지 — 버전 태그
private final AtomicStampedReference<ShipmentState> state =
new AtomicStampedReference<>(new ShipmentState("READY"), 0);
public boolean transition(ShipmentState expected, ShipmentState next) {
int[] stampHolder = new int[1];
ShipmentState current = state.get(stampHolder);
int currentStamp = stampHolder[0];
// 값 + 버전 모두 확인 (ABA 방지)
return state.compareAndSet(
current, next,
currentStamp, currentStamp + 1);
// READY → PROCESSING → READY 거쳐도
// 버전 다름 → 감지
}
record ShipmentState(String name) {}
record Node(int value) {}
}
ABA 문제란?
답:
1. ABA:
시나리오:
문제:
해결:
Atomic 이 synchronized 보다 빠른 이유:
1. 락 없음 (논블로킹)
2. BLOCKED 없음
3. 컨텍스트 스위칭 적음
4. 단일 CPU 명령 (CAS)
락 오버헤드 없음:
synchronized:
- 락 획득/해제
- 경쟁 시 BLOCKED
- 스위칭
Atomic:
- 락 없음
- CAS 명령 (가벼움)
- 스위칭 적음
경쟁 낮을 때:
Atomic:
- CAS 한 번에 성공
- 매우 빠름
synchronized:
- 락 획득/해제 비용
- 상대적 느림
→ 경쟁 낮으면 Atomic 압승
경쟁 높을 때:
Atomic:
- 재시도 ↑
- CPU 사용 (스핀)
synchronized:
- 큐 대기 (CPU 양보)
→ 경쟁 매우 높으면
→ synchronized 가 나을 수도
→ 또는 LongAdder
// 경쟁 높을 때 — LongAdder
LongAdder adder = new LongAdder();
adder.increment(); // 분산 (cell 별)
long sum = adder.sum();
// AtomicLong: 단일 변수 (경쟁 ↑ 재시도)
// LongAdder: 분산 (경쟁 분산, 빠름)
// 카운터 경쟁 높으면 LongAdder
@Service
public class WhyAtomicFaster {
// 경쟁 낮음 — AtomicInteger
private final AtomicInteger lowContention = new AtomicInteger();
public void occasionalUpdate() {
lowContention.incrementAndGet(); // 빠름 (CAS 한 번)
}
// 경쟁 높음 — LongAdder
private final LongAdder highContention = new LongAdder();
public void frequentUpdate() {
highContention.increment(); // 분산 (경쟁 ↓)
}
public long getTotal() {
return highContention.sum(); // 집계
}
// 카운터 경쟁:
// 낮음 → AtomicInteger/Long
// 높음 → LongAdder
}
Atomic이 synchronized보다 빠른 이유는?
답:
1. 빠른 이유:
경쟁 낮을 때:
경쟁 높을 때:
단일 명령:
기본 Atomic:
AtomicInteger: int
AtomicLong: long
AtomicBoolean: boolean
AtomicReference<V>: 참조
AtomicInteger ai = new AtomicInteger(0);
ai.get(); // 읽기
ai.set(5); // 쓰기
ai.incrementAndGet(); // ++x (원자적)
ai.getAndIncrement(); // x++ (원자적)
ai.addAndGet(10); // += 10
ai.compareAndSet(5, 10); // CAS
ai.updateAndGet(x -> x * 2); // 함수 적용 (CAS 재시도)
ai.accumulateAndGet(5, Integer::sum); // 누적
배열 Atomic:
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
- 배열 요소별 원자 연산
- volatile 배열과 다름
고성능 누산기 (Java 8+):
LongAdder: 분산 long 누적
LongAccumulator: 커스텀 누적
DoubleAdder, DoubleAccumulator
- 경쟁 높을 때
- 분산 (cell)
// AtomicReferenceFieldUpdater (필드 원자 갱신)
class Node {
volatile Node next;
static final AtomicReferenceFieldUpdater<Node, Node> NEXT =
AtomicReferenceFieldUpdater.newUpdater(
Node.class, Node.class, "next");
}
// 객체 필드를 원자적으로 (메모리 절약)
@Service
public class AtomicClasses {
// AtomicInteger — 카운터
private final AtomicInteger processedCount = new AtomicInteger();
// AtomicLong — 누적
private final AtomicLong totalBytes = new AtomicLong();
// AtomicReference — 상태
private final AtomicReference<Config> config =
new AtomicReference<>(new Config());
// AtomicBoolean — 플래그 (CAS 가능)
private final AtomicBoolean initialized = new AtomicBoolean(false);
// LongAdder — 고경쟁 카운터
private final LongAdder requestCount = new LongAdder();
public void process(Shipment shipment) {
processedCount.incrementAndGet();
totalBytes.addAndGet(shipment.getSize());
requestCount.increment(); // 고경쟁
}
public boolean initOnce() {
// 한 번만 초기화 (CAS)
return initialized.compareAndSet(false, true);
}
record Config() {}
}
주요 Atomic 클래스는?
답:
1. 기본:
메서드:
배열:
고성능:
| Q | 핵심 답변 |
|---|---|
| CAS 4단계? | 읽기/계산/비교/교체or재시도 |
| 논블로킹? | 락 없이 재시도 |
| 재시도? | 충돌 시 다시 |
| 은행 예시? | 100→150→200 (재시도) |
| 3종 비교? | sync(락)/volatile(가시성)/Atomic(CAS) |
| ABA? | A→B→A 못 감지 |
| ABA 해결? | AtomicStampedReference |
| Atomic 빠른 이유? | 락 없음 |
| 무한 재시도? | 이론적, 실제 성공 |
| LongAdder? | 고경쟁 분산 |
Q1. CAS? → Compare And Swap
Q2. 4단계? → 읽기/계산/비교/교체or재시도
Q3. 1단계? → 현재 값 읽기 (A)
Q4. 2단계? → 새 값 계산 (B)
Q5. 3단계? → 메모리 == A 비교
Q6. 4단계? → 같으면 교체, 다르면 재시도
Q7. compareAndSet? → 비교+교체
Q8. 하드웨어? → CPU 명령 (CMPXCHG)
Q9. 원자적? → 단일 명령
Q10. 기대값? → 읽은 현재 값 (A)
Q11. 교체 성공? → 현재 == 기대값
Q12. 교체 실패? → 다름 (재시도)
Q13. 단일 변수? → CAS 적합
Q14. 논블로킹? → 락 없이
Q15. vs 블로킹? → BLOCKED 없음
Q16. 낙관적? → 일단 시도
Q17. 비관적? → synchronized (미리 락)
Q18. 재시도? → 충돌 시 다시
Q19. 재시도 루프? → 성공까지 스핀
Q20. 무한 반복? → 이론적, 실제 성공
Q21. 라이브락? → 계속 충돌 (드묾)
Q22. 데드락? → CAS 는 없음 (락 X)
Q23. 경쟁 낮음? → 재시도 적음
Q24. 경쟁 높음? → 재시도 ↑
Q25. 논블로킹 장점? → 대기/데드락 없음
Q26. synchronized 가시성? → ✅
Q27. synchronized 원자성? → ✅
Q28. synchronized 방식? → 락 (블로킹)
Q29. volatile 가시성? → ✅
Q30. volatile 원자성? → ❌
Q31. volatile 방식? → 메모리 동기화
Q32. Atomic 가시성? → ✅
Q33. Atomic 원자성? → ✅
Q34. Atomic 방식? → CAS (논블로킹)
Q35. 가장 느림? → synchronized
Q36. 은행 스레드1? → 100→150 성공
Q37. 은행 스레드2? → 150≠100 재시도→200
Q38. 손실? → 없음 (CAS 재시도)
Q39. ABA? → A→B→A 못 감지
Q40. ABA 시나리오? → 중간 B 거침
Q41. ABA 문제? → 참조/구조
Q42. ABA 해결? → AtomicStampedReference
Q43. 스탬프? → 버전 태그
Q44. AtomicInteger? → int 원자
Q45. AtomicReference? → 참조 원자
Q46. incrementAndGet? → ++x
Q47. updateAndGet? → 함수 (CAS 재시도)
Q48. LongAdder? → 고경쟁 분산
Q49. Atomic 빠른 이유? → 락 없음
Q50. 단일 명령? → CAS (CPU)
50 / 50 → Atomic + CAS 마스터
45-49 → 거의 마스터
40-44 → 복습
< 40 → Unit 2.4 재학습
답:
답:
답:
답:
답:
1. CAS 4단계
2. 3종 비교
3. 주의
🚀 Phase 2 — 동시성 안전 도구 3종 비교
✅ Unit 2.1 두 가지 동시성 문제 구분
✅ Unit 2.2 synchronized
✅ Unit 2.3 volatile
✅ Unit 2.4 Atomic + CAS (★ 마스터) ← 여기, Phase 2 완주
→ 가시성 vs 원자성
→ synchronized / volatile / Atomic
→ CAS 알고리즘
✅ Part A — 동시성 마무리
✅ Phase 1 — 스레드 풀의 필요성 (3 Unit)
✅ Phase 2 — 동시성 안전 도구 3종 (4 Unit)
→ 4주차 동시성 + 5주차 Atomic/CAS
→ 자바 동시성 완성
→ 이제 Spring 으로 (Part B)
이제 토비의 스프링: 객체 설계의 진화 로 진입합니다.
Part B — 토비의 스프링
Phase 3 — 전통 DAO의 문제
Unit 3.1 — DAO란 무엇인가
Unit 3.2 — 전통 DAO의 코드
Unit 3.3 — 책임 혼재
✅ Part A — 동시성 마무리 (7 Unit) ← 완주
✅ Phase 1 (3) + Phase 2 (4, 2.4 ★마스터)
⏭ Part B — 토비의 스프링 (Phase 3~8, 19 Unit)
총: 7/26 Unit
★ 마스터 Unit — Atomic + CAS 완료
🏆 Phase 2 완주 + 🎓 Part A (동시성 마무리) 완주