F-LAB JAVA · 5주차 · Phase 2 · 동시성 안전 도구 3종 비교
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
synchronized 는 한 번에 한 스레드만 임계 영역에 진입시켜 원자성을 보장하고, 락 획득·해제 시점에 메인 메모리와 동기화하여 가시성도 보장하는, 가장 안전하지만 다른 스레드를 BLOCKED 시켜 가장 비싼 도구다.
원자성 보장 — synchronized 블록/메서드는 모니터 락 (monitor lock) 을 통해 한 번에 한 스레드만 진입시키므로, 복합 연산 (count++ 등) 이 다른 스레드의 개입 없이 완료된다.
가시성 보장 — 락을 획득할 때 캐시를 무효화하고 메인 메모리에서 읽으며, 락을 해제할 때 변경 사항을 메인 메모리에 반영하므로 다른 스레드가 최신 값을 본다.
성능 비용 — 락을 잡은 스레드가 임계 영역을 실행하는 동안 다른 스레드는 BLOCKED 상태로 대기해야 하므로 병렬성이 떨어지고, 컨텍스트 스위칭·락 경쟁 비용이 발생한다.
또한 synchronized 는 전체 락 (객체/메서드 단위) 이라 세밀하지 못하고, 읽기만 하는 경우에도 다른 스레드가 쓰는 중이면 가시성·일관성을 위해 동기화가 필요하다.
synchronized = 1인용 화장실:
원자성 (한 명만):
- 한 번에 한 명만 입장
- 문 잠금 (락)
- 안에서 볼일 (임계 영역)
- 끝나면 다음 사람
가시성 (상태 동기화):
- 들어갈 때 현재 상태 확인 (메인 메모리 읽기)
- 나올 때 상태 반영 (메인 메모리 쓰기)
- 다음 사람이 최신 상태 봄
성능 비용 (대기):
- 한 명 쓰는 동안
- 나머지 줄 서서 대기 (BLOCKED)
- 병렬 X (한 명씩)
전체 락 단점:
- 화장실 전체 잠금
- 세면대만 쓰고 싶어도 못 씀
- 너무 큰 락
→ 가장 안전 (충돌 X)
→ 가장 느림 (한 명씩)
→ synchronized = 모니터 락 (한 명씩), 원자성 + 가시성 보장, 대신 BLOCKED (느림).
1. 원자성 보장 원리
2. 가시성 보장 원리
3. 성능 비용 (BLOCKED)
4. 가장 안전, 가장 비쌈
5. 전체 락 단점
6. 읽기에도 필요한 경우
7. 메서드 락 vs 블록 락
8. 모니터와 락
9. 면접 + 자기 점검
원자성 보장:
synchronized:
- 한 번에 한 스레드만 진입
- 모니터 락
- 다른 스레드 대기
→ 복합 연산도 안전
임계 영역 (Critical Section):
synchronized 블록/메서드:
- 한 스레드만 실행
- 다른 스레드 대기 (BLOCKED)
→ 원자적 실행 (중간 개입 X)
// synchronized 로 count++ 안전
private int count = 0;
public synchronized void increment() {
count++; // read-modify-write 가 원자적
// 한 스레드만 진입 → 손실 X
}
// 여러 연산도 원자적
private int balance = 0;
private final List<String> log = new ArrayList<>();
public synchronized void transfer(int amount) {
balance += amount; // 1
log.add("transfer: " + amount); // 2
// 1, 2 가 함께 원자적 (한 스레드)
}
@Service
public class SynchronizedAtomicity {
private int processedCount = 0;
private BigDecimal totalFreight = BigDecimal.ZERO;
// 복합 연산 원자적
public synchronized void recordProcessing(Shipment shipment) {
processedCount++; // 1
totalFreight = totalFreight.add(shipment.getWeight()); // 2
// 1, 2 함께 (한 스레드만)
// 다른 스레드 개입 X
}
public synchronized int getCount() {
return processedCount;
}
}
synchronized가 원자성을 보장하는 원리는?
답:
1. 원리:
임계 영역:
count++:
복합 연산:
가시성 보장:
synchronized:
- 락 획득 시: 캐시 무효화 → 메인 메모리 읽기
- 락 해제 시: 변경 → 메인 메모리 반영
→ 다른 스레드 최신 값
락 획득 시:
- 캐시 무효화
- 메인 메모리에서 읽기
- 최신 값 보장
→ 들어갈 때 최신 상태
락 해제 시:
- 변경 사항 메인 메모리에 쓰기
- 다른 스레드가 볼 수 있게
→ 나올 때 변경 반영
happens-before:
락 해제 happens-before 락 획득:
- 이전 스레드 변경
- 다음 스레드에 보임
→ 가시성 보장 (JMM)
@Service
public class SynchronizedVisibility {
private boolean ready = false;
private Shipment data;
// 쓰기 (락 해제 시 메인 메모리 반영)
public synchronized void prepare(Shipment shipment) {
data = shipment; // 변경
ready = true; // 변경
// 락 해제 → 메인 메모리 (가시성)
}
// 읽기 (락 획득 시 메인 메모리 읽기)
public synchronized Shipment getData() {
// 락 획득 → 최신 값 읽기 (가시성)
if (ready) {
return data; // 최신
}
return null;
}
}
synchronized가 가시성을 보장하는 원리는?
답:
1. 메모리 동기화:
획득 시:
해제 시:
happens-before:
성능 비용 — BLOCKED:
락 잡은 스레드 실행 중:
- 다른 스레드 BLOCKED
- 락 대기
- 병렬 X
병렬성 저하:
임계 영역:
- 한 스레드만
- 나머지 대기
→ 직렬화 (순차)
→ 병렬 이점 ↓
synchronized 비용:
1. 락 획득/해제
- CAS 또는 시스템 콜
2. BLOCKED 대기
- 컨텍스트 스위칭
3. 경쟁 (contention)
- 여러 스레드 대기
4. 직렬화
- 병렬성 ↓
경쟁 영향:
경쟁 낮음:
- 락 빨리 획득
- 비용 적음
경쟁 높음:
- 많은 스레드 대기
- 스위칭 폭증
- 비용 ↑
@Service
public class SynchronizedCost {
private int count = 0;
// ❌ 큰 임계 영역 (병렬성 ↓)
public synchronized void wrongHeavy(Shipment shipment) {
count++;
heavyComputation(shipment); // 무거운 작업도 락 안에서
// 다른 스레드 오래 BLOCKED
}
// ✓ 작은 임계 영역 (병렬성 ↑)
public void betterMinimal(Shipment shipment) {
BigDecimal result = heavyComputation(shipment); // 락 밖
synchronized (this) {
count++; // 락 안 (최소)
}
// 무거운 작업은 병렬, 카운터만 동기화
}
private BigDecimal heavyComputation(Shipment s) { return s.getWeight(); }
}
성능 비용 (BLOCKED) 의 의미는?
답:
1. BLOCKED:
병렬성 저하:
비용:
경쟁:
가장 안전:
synchronized:
- 가시성 ✅
- 원자성 ✅
- 복합 연산 OK
- 여러 변수 일관성
→ 확실한 안전
가장 비쌈:
synchronized:
- BLOCKED (대기)
- 병렬성 ↓
- 락 경쟁
→ 성능 비용 큼
안전 vs 성능 트레이드오프:
synchronized:
+ 가장 안전
- 가장 느림
volatile:
+ 빠름
- 가시성만
Atomic:
+ 빠름 (CAS)
- 단일 변수
synchronized 선택:
- 복잡한 복합 연산
- 여러 변수 일관성
- 정확성 > 성능
- 단순함 우선
→ 안전이 최우선일 때
synchronized 최적화:
- 임계 영역 최소화
- 락 분할 (여러 락)
- 읽기 많으면 ReadWriteLock
- 단일 변수면 Atomic
@Service
public class SafestButSlowest {
// 복잡한 일관성 → synchronized (안전 우선)
private final Map<Long, Shipment> shipments = new HashMap<>();
private BigDecimal totalWeight = BigDecimal.ZERO;
private int count = 0;
public synchronized void addShipment(Shipment shipment) {
// 여러 상태 일관성 (synchronized 가 적합)
shipments.put(shipment.getId(), shipment);
totalWeight = totalWeight.add(shipment.getWeight());
count++;
// 세 변수 일관성 (한 스레드만)
}
public synchronized Summary getSummary() {
return new Summary(count, totalWeight);
// 일관된 스냅샷
}
record Summary(int count, BigDecimal totalWeight) {}
}
"가장 안전하지만 가장 비싼 도구" 인 이유는?
답:
1. 가장 안전:
가장 비쌈:
트레이드오프:
선택:
전체 락:
synchronized 메서드/객체:
- 객체 전체 락
- 세밀하지 못함
- 관련 없는 작업도 대기
// 전체 락 문제
public class Account {
private int balance;
private String name;
public synchronized void deposit(int amount) {
balance += amount; // balance 만 관련
}
public synchronized void setName(String name) {
this.name = name; // name 만 관련
}
// deposit 과 setName 이 같은 락
// → 동시 실행 불가 (불필요한 대기)
}
// 락 분할 (세밀하게)
public class Account {
private int balance;
private String name;
private final Object balanceLock = new Object();
private final Object nameLock = new Object();
public void deposit(int amount) {
synchronized (balanceLock) { // balance 락
balance += amount;
}
}
public void setName(String name) {
synchronized (nameLock) { // name 락 (별도)
this.name = name;
}
}
// deposit 과 setName 동시 가능
}
전체 락 vs 분할:
Hashtable (전체 락):
- 모든 메서드 synchronized
- 동시성 ↓
ConcurrentHashMap (분할):
- 버킷/세그먼트 단위
- 동시성 ↑
→ 분할이 효율적
@Service
public class FullLockDrawback {
// ❌ 전체 락 (세밀하지 못함)
public class ShipmentManagerBad {
private int bookingCount;
private int shipmentCount;
public synchronized void addBooking() { bookingCount++; }
public synchronized void addShipment() { shipmentCount++; }
// 둘이 같은 락 → 동시 불가
}
// ✓ 락 분할 또는 Atomic
public class ShipmentManagerGood {
private final AtomicInteger bookingCount = new AtomicInteger();
private final AtomicInteger shipmentCount = new AtomicInteger();
public void addBooking() { bookingCount.incrementAndGet(); }
public void addShipment() { shipmentCount.incrementAndGet(); }
// 독립 (동시 가능)
}
}
synchronized의 "전체 락" 단점은?
답:
1. 전체 락:
문제:
해결:
비교:
읽기에도 synchronized 필요:
다른 스레드가 쓰는 중:
- 읽기도 동기화 필요
- 가시성 (최신 값)
- 일관성 (중간 상태 방지)
// 읽기 동기화 (가시성)
private int value = 0;
public synchronized void write(int v) {
value = v;
}
public synchronized int read() { // 읽기도 synchronized
return value;
// 동기화 없으면 옛 값 (가시성)
}
// 일관성 (중간 상태 방지)
private int x, y;
public synchronized void update(int newX, int newY) {
x = newX; // 중간 상태 (x 변경, y 안 됨)
y = newY;
}
public synchronized Point read() { // 읽기 동기화
return new Point(x, y);
// 동기화 없으면 중간 상태 (x 새, y 옛)
}
양쪽 동기화:
쓰기만 synchronized:
- 읽기가 중간/옛 값
읽기도 synchronized:
- 일관된 최신 값
→ 읽기/쓰기 모두 동기화
@Service
public class ReadSynchronization {
private int totalShipments = 0;
private BigDecimal totalWeight = BigDecimal.ZERO;
// 쓰기
public synchronized void add(Shipment shipment) {
totalShipments++;
totalWeight = totalWeight.add(shipment.getWeight());
}
// 읽기도 synchronized (일관성 + 가시성)
public synchronized Stats getStats() {
return new Stats(totalShipments, totalWeight);
// 동기화 없으면:
// - 옛 값 (가시성)
// - 중간 상태 (count 새, weight 옛)
}
record Stats(int count, BigDecimal weight) {}
}
읽기만 하는데도 synchronized가 필요한 경우는?
답:
1. 필요:
가시성:
일관성:
양쪽:
// 메서드 락 (메서드 전체)
public synchronized void method() {
// 전체가 임계 영역
}
// 락 객체: this (인스턴스 메서드)
// 락 객체: 클래스 (static 메서드)
// 블록 락 (일부만)
public void method() {
// 락 밖 (병렬)
doSomething();
synchronized (lock) {
// 락 안 (임계 영역, 최소)
criticalSection();
}
// 락 밖
doMore();
}
| 항목 | 메서드 락 | 블록 락 |
|---|---|---|
| 범위 | 메서드 전체 | 일부 |
| 락 객체 | this/클래스 | 지정 |
| 세밀함 | 낮음 | 높음 |
| 성능 | 낮음 | 높음 |
블록 락 권장:
임계 영역 최소화:
- 락 안 최소
- 락 밖 병렬
- 성능 ↑
락 객체 지정:
- 세밀한 제어
// 락 객체 선택
private final Object lock = new Object(); // 전용 락
public void method() {
synchronized (lock) { // 전용 락 객체
// ...
}
}
// this 대신 전용 객체 (외부 간섭 방지)
@Service
public class MethodVsBlockLock {
private final Object countLock = new Object();
private int count = 0;
// ❌ 메서드 락 (전체)
public synchronized void processWrong(Shipment shipment) {
validate(shipment); // 무거움 (락 불필요)
calculate(shipment); // 무거움 (락 불필요)
count++; // 락 필요
// 전체 락 → 병렬성 ↓
}
// ✓ 블록 락 (최소)
public void processBetter(Shipment shipment) {
validate(shipment); // 락 밖 (병렬)
calculate(shipment); // 락 밖 (병렬)
synchronized (countLock) {
count++; // 락 안 (최소)
}
}
private void validate(Shipment s) { }
private void calculate(Shipment s) { }
}
메서드 락 vs 블록 락의 차이는?
답:
1. 메서드 락:
블록 락:
권장:
락 객체:
모니터 (Monitor):
각 객체가 가진 동기화 메커니즘.
- 모니터 락 (intrinsic lock)
- synchronized 가 사용
모니터 락:
모든 자바 객체:
- 모니터 락 보유
- synchronized 가 획득/해제
한 스레드만 보유:
- 다른 스레드 대기
// 재진입 (reentrant)
public synchronized void a() {
b(); // 같은 락 재획득 (재진입 OK)
}
public synchronized void b() {
// a 가 잡은 락, b 도 같은 락
// 재진입 가능 (데드락 X)
}
// 자바 모니터 락 = 재진입 가능
모니터와 wait/notify:
wait/notify 도 모니터:
- synchronized 안에서만
- 모니터 락 + 대기 집합
(4주차 Phase 6)
@Service
public class MonitorAndLock {
// 모니터 락 (this)
public synchronized void method1() {
// this 의 모니터 락
}
// 재진입
public synchronized void outer(Shipment shipment) {
inner(shipment); // 같은 락 재진입
}
public synchronized void inner(Shipment shipment) {
// outer 가 잡은 락, inner 재진입 OK
process(shipment);
}
// 전용 락 객체의 모니터
private final Object lock = new Object();
public void withDedicatedLock() {
synchronized (lock) { // lock 객체의 모니터
// ...
}
}
private void process(Shipment s) { }
}
모니터와 락의 관계는?
답:
1. 모니터:
모니터 락:
재진입:
wait/notify:
| Q | 핵심 답변 |
|---|---|
| 원자성 보장? | 한 스레드만 (모니터 락) |
| 가시성 보장? | 획득/해제 시 동기화 |
| 성능 비용? | BLOCKED (병렬 X) |
| 가장 안전/비쌈? | 둘 다 보장, 느림 |
| 전체 락 단점? | 세밀하지 못함 |
| 읽기 동기화? | 가시성/일관성 |
| 메서드 vs 블록? | 전체 vs 최소 |
| 모니터? | 객체 동기화 |
| 재진입? | 같은 락 재획득 |
| 최적화? | 임계 영역 최소 |
답:
답:
답:
답:
답:
1. 둘 다 보장
2. 성능 비용
3. 주의
이번 Unit에서 synchronized 를 봤다면, 다음은 volatile (가시성만).
🚀 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 — 동시성 안전 도구 (2/4 진행)
총: 5/26 Unit