F-LAB JAVA · 4주차 · Phase 4 · 동기화: synchronized와 메모리 가시성
★ 마스터 Unit — 면접 단골 (★★★)
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
모니터 락 (Monitor Lock, = intrinsic lock / 고유 락) 은 모든 자바 객체가 내부적으로 하나씩 가지는 락으로, synchronized 가 이 락을 사용하여 상호 배제를 구현한다.
스레드가 synchronized 에 진입하려면 그 객체의 모니터 락 획득을 시도하며, 성공하면 RUNNABLE 로 진입, 실패하면 (다른 스레드가 보유 중) BLOCKED 상태로 대기 한다.
synchronized 메서드/블록이 종료되면 (정상 또는 예외) 락이 자동으로 반환 되고, 대기 중이던 BLOCKED 스레드 중 하나가 락을 획득한다.
100개 스레드가 같은 synchronized 블록 진입을 시도하면 1개만 성공하고 99개는 BLOCKED 되며, 락 획득 순서는 자바 표준에 정의되어 있지 않다 (공정성 X) — 먼저 대기한 스레드가 먼저 받는다는 보장이 없다.
synchronized 의 단점은 (1) 무한 대기 (타임아웃 불가), (2) 인터럽트 불가 (대기 중 깰 수 없음), (3) 공정성 보장 X 이며, 이를 극복하는 도구가 Phase 5 의 ReentrantLock 이다.
모니터 락 = 회의실 열쇠 (객체마다 1개):
진입 시도:
- 열쇠 있으면 → 들어감 (RUNNABLE)
- 열쇠 없으면 → 문 앞 대기 (BLOCKED)
종료 시:
- 열쇠 반납
- 대기자 중 1명이 가져감
100명 대기 (락 경쟁):
- 열쇠 1개
- 1명만 들어감
- 99명 문 앞 대기 (BLOCKED)
공정성 X (먼저 온 순서 보장 X):
- 열쇠 반납 시
- 누가 가져갈지 OS 가 결정
- 먼저 기다린 사람이 못 받을 수도
- "새치기" 가능 (barging)
단점:
- 무한 대기 (포기 불가)
- 누가 두드려도 (인터럽트) 안 나옴
- 줄 순서 (공정성) 없음
→ 모니터 락 = 객체별 열쇠, 100명 중 1명, 공정성 없음.
1. 모니터 락의 정의
2. 모든 객체가 모니터 락
3. 락 획득 과정 (성공/실패)
4. BLOCKED 대기 메커니즘
5. 락 자동 반환
6. 락 경쟁 시나리오 (100 스레드)
7. 공정성 (Fairness)
8. synchronized의 단점 3가지
9. 면접 + 자기 점검 + 마스터 50문항
모니터 락 (Monitor Lock):
= Intrinsic Lock (고유 락)
= Object Lock
모든 자바 객체가 내부적으로 가지는 락.
synchronized 가 이 락을 사용.
특징:
- 객체당 1개
- 상호 배제
- 재진입 가능
모니터 (Monitor):
동기화 메커니즘.
- 락 (Lock) — 상호 배제
- 대기 집합 (Wait Set) — wait/notify
자바:
- 모든 객체가 모니터
- synchronized = 락 사용
- wait/notify = 대기 집합 (Phase 6)
"intrinsic" (고유):
- 객체에 내재된 락
- 별도 객체 생성 불필요
- 모든 객체에 기본 내장
대조:
- ReentrantLock (명시적 락)
- 별도 객체 생성 (new ReentrantLock)
Object obj = new Object();
// obj 의 모니터 락 사용
synchronized (obj) {
// obj 의 락 보유
}
// 모든 객체 가능
synchronized (this) { } // this 의 락
synchronized (myList) { } // myList 의 락
synchronized (String.class) { } // Class 객체 락
모니터 락 구성:
1. 소유 스레드 (Owner)
- 현재 락 보유 스레드
2. 재진입 카운트 (Recursion Count)
- 같은 스레드 재획득 횟수
3. 진입 대기 집합 (Entry Set)
- 락 대기 스레드 (BLOCKED)
4. 대기 집합 (Wait Set)
- wait() 한 스레드 (WAITING)
public class MonitorLockBasics {
private int count = 0;
private final Object lock = new Object(); // lock 의 모니터
public void increment() {
synchronized (lock) { // lock 의 모니터 락 사용
count++;
}
}
// this 의 모니터
public synchronized void incrementThis() {
count++; // this 의 락
}
// 모든 객체가 모니터 락 보유
// - lock (Object)
// - this (ShipmentService)
// - String, List, 등 모두
}
모니터 락의 정의는?
답:
1. 정의:
모니터:
intrinsic:
구성:
모든 자바 객체가 모니터 락:
- Object 부터 상속
- 객체 헤더에 락 정보
- 별도 설정 불필요
따라서:
- 어떤 객체든 synchronized 가능
- new Object(), this, List, 등
객체 헤더 구조:
┌──────────────────────┐
│ Mark Word │ ← 락 정보, 해시, GC age
├──────────────────────┤
│ Klass Pointer │ ← 클래스 메타데이터
├──────────────────────┤
│ (배열이면 길이) │
└──────────────────────┘
┌──────────────────────┐
│ 인스턴스 데이터 │ ← 필드
└──────────────────────┘
Mark Word (락 정보):
객체 헤더의 일부.
락 상태에 따라 내용 변함:
- Unlocked: 해시코드, GC age
- Biased: 편향 스레드 ID
- Lightweight: 락 레코드 포인터
- Heavyweight: 모니터 포인터
- GC: 마킹
→ 락 상태를 Mark Word 로 표현
락 상태 전이 (Java 6+ 최적화):
No Lock
↓ (단일 스레드 접근)
Biased Lock (편향 락)
↓ (다른 스레드 경합)
Lightweight Lock (경량 락, CAS)
↓ (경합 심화)
Heavyweight Lock (중량 락, 모니터)
- 경합 적으면 가벼움 (빠름)
- 경합 심하면 무거움 (OS 모니터)
락 상태별 비용:
Biased Lock:
- 거의 무비용
- 단일 스레드 반복
Lightweight Lock:
- CAS 연산
- 경합 적을 때
Heavyweight Lock:
- OS 모니터
- 컨텍스트 스위칭
- 비쌈
→ JVM 이 자동 최적화
public class ObjectMonitorExample {
// 모든 객체가 모니터 락
private final Object lock1 = new Object();
private final List<Shipment> list = new ArrayList<>();
private final Map<Long, Shipment> map = new HashMap<>();
public void useVariousLocks() {
synchronized (lock1) { // Object 의 모니터
// ...
}
synchronized (list) { // ArrayList 의 모니터
list.add(shipment);
}
synchronized (map) { // HashMap 의 모니터
map.put(1L, shipment);
}
// 모두 각자의 모니터 락
// 독립적
}
}
모든 객체가 모니터 락을 가지는 의미는?
답:
1. 모든 객체:
객체 헤더:
Mark Word:
락 상태 전이:
synchronized 진입 시:
1. 락 획득 시도 (monitorenter)
2. 성공:
- 락 보유
- RUNNABLE (진입)
3. 실패 (다른 스레드 보유):
- BLOCKED (대기)
락 획득 성공:
스레드 A:
synchronized (lock) 진입
↓ monitorenter
락 비어있음 → 획득
↓
RUNNABLE (임계 영역 실행)
↓ monitorexit
락 반납
락 획득 실패:
스레드 B (A 가 락 보유 중):
synchronized (lock) 진입
↓ monitorenter
락 있음 (A 보유) → 획득 실패
↓
BLOCKED (진입 대기 집합)
↓ (A 가 반납)
락 획득 → RUNNABLE
바이트코드:
synchronized (lock) {
// ...
}
→
monitorenter // 락 획득
// 임계 영역
monitorexit // 락 반납 (정상)
monitorexit // 락 반납 (예외 시)
락 획득 (A 성공, B 실패):
lock 모니터:
Owner: A (보유)
Entry Set: [B] (대기)
스레드 A: [monitorenter 성공][임계 영역][monitorexit]
스레드 B: [monitorenter 실패 → BLOCKED────][획득][실행]
↑ A 반납 후
@Service
public class LockAcquisition {
private int counter = 0;
private final Object lock = new Object();
public void process() {
// monitorenter (락 획득 시도)
synchronized (lock) {
// 성공: RUNNABLE (여기 실행)
// 실패: BLOCKED (다른 스레드 대기)
counter++;
doWork();
}
// monitorexit (락 반납)
}
private void doWork() {
// 임계 영역
// 이 동안 다른 스레드 BLOCKED
}
}
락 획득 과정 (성공/실패)은?
답:
1. 시도:
성공:
실패:
바이트코드:
BLOCKED 상태:
모니터 락을 획득하지 못해
진입 대기 집합 (Entry Set) 에서 대기.
특징:
- CPU 사용 X (대기)
- 락 반납 시 깨어남
- 인터럽트로 못 깨움
Entry Set (진입 대기 집합):
락을 기다리는 스레드들.
- BLOCKED 상태
- 락 반납 시 경쟁
vs Wait Set (대기 집합):
- wait() 한 스레드
- WAITING 상태
- notify 로 깨어남 (Phase 6)
모니터 구조:
모니터 (lock)
┌─────────────────────┐
│ Owner: 스레드 A │ ← 현재 보유
├─────────────────────┤
│ Entry Set: │ ← 락 대기 (BLOCKED)
│ [B, C, D] │
├─────────────────────┤
│ Wait Set: │ ← wait() (WAITING)
│ [E, F] │
└─────────────────────┘
A 반납 시 → Entry Set 에서 하나 선택
BLOCKED → RUNNABLE:
락 보유 스레드가 반납 (monitorexit)
↓
Entry Set 에서 하나 선택
↓
선택된 스레드 락 획득
↓
RUNNABLE
- 선택은 OS/JVM (공정성 X)
- 인터럽트로 안 깨어남
BLOCKED vs WAITING:
BLOCKED:
- 락 대기 (Entry Set)
- synchronized 진입 실패
- 락 반납 시 깨어남
WAITING:
- wait() (Wait Set)
- 명시적 대기
- notify 시 깨어남
핵심:
- BLOCKED: 락 경쟁
- WAITING: 조건 대기
@Service
public class BlockedMechanism {
private final Object lock = new Object();
public void longOperation() {
synchronized (lock) {
// 오래 점유
try { Thread.sleep(5000); } catch (Exception e) {}
// 이 동안 다른 스레드 BLOCKED
}
}
// 모니터링 — BLOCKED 확인
public void monitorBlocked(List<Thread> threads) {
long blockedCount = threads.stream()
.filter(t -> t.getState() == Thread.State.BLOCKED)
.count();
if (blockedCount > 0) {
log.warn("{} threads blocked on lock", blockedCount);
// jstack 으로 어떤 락인지 확인
}
}
}
BLOCKED 대기 메커니즘은?
답:
1. BLOCKED:
Entry Set:
깨어남:
vs WAITING:
락 자동 반환:
synchronized 블록/메서드 종료 시
락 자동 반환.
종료:
- 정상 종료 (return)
- 예외 발생 (throw)
- 둘 다 반환 보장
synchronized (lock) {
count++;
// 블록 끝
}
// monitorexit (자동 반환)
synchronized (lock) {
count++;
throw new RuntimeException(); // 예외!
// 그래도 monitorexit (반환 보장)
}
// 예외 전파되지만 락은 반환
// 바이트코드:
// monitorenter
// try {
// // 임계 영역
// } finally {
// monitorexit // 예외 시에도
// }
자동 반환의 장점:
- 명시적 unlock 불필요
- 예외 시에도 안전
- 데드락 방지 (락 잔류 X)
대조 (ReentrantLock):
- 명시적 unlock 필요
- try-finally 필수
- 깜박하면 데드락
자동 반환:
정상:
monitorenter → [임계 영역] → monitorexit
↑ 자동
예외:
monitorenter → [임계 영역 중 예외] → monitorexit
↑ 자동 (예외 시에도)
→ 락 항상 반환 (안전)
@Service
public class AutoRelease {
private int counter = 0;
private final Object lock = new Object();
// 자동 반환 (예외 시에도)
public void process(Shipment shipment) {
synchronized (lock) {
counter++;
validate(shipment); // 예외 가능
// 예외 발생해도 락 자동 반환
}
// 데드락 위험 없음
}
private void validate(Shipment s) {
if (s == null) {
throw new IllegalArgumentException();
// 예외! 하지만 락은 반환됨
}
}
// synchronized 장점:
// - unlock 깜박할 일 없음
// - 예외 시에도 안전
// (ReentrantLock 은 try-finally 필수 — Phase 5)
}
락 자동 반환은?
답:
1. 자동 반환:
예외 시:
장점:
vs Lock:
100 스레드 락 경쟁:
100개 스레드가 같은 synchronized 블록 진입 시도.
결과:
- 1개만 성공 (락 획득)
- 99개 BLOCKED (대기)
→ 직렬화 (하나씩 처리)
public class HundredThreadsContention {
private int counter = 0;
private final Object lock = new Object();
public void demo() throws InterruptedException {
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Thread t = new Thread(() -> {
synchronized (lock) { // 100개 경쟁
counter++;
sleep(10); // 점유
}
});
t.start();
threads.add(t);
}
for (Thread t : threads) t.join();
System.out.println("Counter: " + counter); // 정확히 100
// 하지만 100 × 10ms = 약 1초 (직렬화)
}
}
100 스레드 직렬화:
락 1개 → 한 번에 1개만 진입.
스레드 1: [락][10ms]
스레드 2: [BLOCKED][락][10ms]
스레드 3: [BLOCKED][락][10ms]
...
전체 시간 = 100 × 10ms = 1초
(병렬성 없음, 직렬)
경합 (Contention) 의 영향:
높은 경합:
- 많은 BLOCKED
- 직렬화
- 처리량 ↓
- 컨텍스트 스위칭 ↑
낮은 경합:
- 적은 대기
- 빠른 처리
→ 경합 줄이기:
- 락 분리
- 범위 최소화
- lock-free (Atomic)
락 경쟁:
lock 모니터
Owner: 스레드 1
Entry Set: [2,3,4,...,100] ← 99개 BLOCKED
스레드 1: [실행]
스레드 1 종료 → 반납
→ Entry Set 에서 1개 선택 (예: 스레드 50)
→ 스레드 50 실행
...
순차적 (직렬화)
@Service
public class ContentionExample {
private int processedCount = 0;
private final Object lock = new Object();
// ❌ 높은 경합 (모든 요청이 같은 락)
public void processWithContention(Shipment shipment) {
synchronized (lock) {
processedCount++;
doProcess(shipment); // 시간 걸림
// 모든 스레드 직렬화
}
}
// ✓ 경합 줄이기 — Atomic
private final AtomicInteger atomicCount = new AtomicInteger();
public void processLowContention(Shipment shipment) {
doProcess(shipment); // 병렬
atomicCount.incrementAndGet(); // lock-free
}
// ✓ 경합 줄이기 — 락 분리
private final Object[] locks = new Object[16];
{
for (int i = 0; i < 16; i++) locks[i] = new Object();
}
public void processStriped(Shipment shipment) {
int stripe = (int)(shipment.getId() % 16);
synchronized (locks[stripe]) { // 분산
doProcess(shipment);
}
// 다른 stripe → 동시 가능
}
private void doProcess(Shipment s) { }
}
락 경쟁 시나리오 (100 스레드)는?
답:
1. 결과:
직렬화:
경합 영향:
줄이기:
공정성 (Fairness):
락 획득 순서가
대기 순서와 일치하는가.
공정 (Fair):
- 먼저 대기 → 먼저 획득 (FIFO)
비공정 (Non-fair):
- 순서 보장 X
- 새치기 가능 (barging)
synchronized 의 공정성:
공정성 보장 X (Non-fair).
- 락 획득 순서 미정의
- 먼저 대기한 스레드가
먼저 받는다는 보장 없음
- OS/JVM 이 결정
비공정의 문제:
1. 기아 (Starvation)
- 특정 스레드가 계속 못 받음
- 무한 대기 가능
2. 예측 불가
- 어느 스레드가 받을지 모름
3. 순서 보장 안 됨
- 처리 순서 중요하면 문제
장점:
- 성능 (공정성 비용 없음)
- 처리량 ↑
기아 (Starvation):
스레드 A, B, C 가 락 경쟁:
- A 가 자주 받음
- C 가 계속 못 받음
- C 기아 상태
비공정 락:
- 운 나쁘면 특정 스레드 기아
- 무한 대기 가능
// synchronized 는 비공정
synchronized (lock) { } // 공정성 X
// 공정성 필요하면 ReentrantLock (Phase 5)
ReentrantLock fairLock = new ReentrantLock(true); // 공정 모드
fairLock.lock();
try {
// FIFO 순서
} finally {
fairLock.unlock();
}
// 단, 공정 모드는 성능 ↓ (순서 관리 비용)
@Service
public class FairnessExample {
private final Object lock = new Object();
// synchronized — 비공정 (순서 보장 X)
public void processNonFair(Shipment shipment) {
synchronized (lock) {
// 먼저 대기한 스레드가 먼저 받는다는 보장 X
doProcess(shipment);
}
}
// 공정성 필요 시 — ReentrantLock(true) (Phase 5)
private final ReentrantLock fairLock = new ReentrantLock(true);
public void processFair(Shipment shipment) {
fairLock.lock(); // FIFO 순서
try {
doProcess(shipment);
} finally {
fairLock.unlock();
}
// 순서 보장 (단 성능 ↓)
}
private void doProcess(Shipment s) { }
}
공정성 (Fairness)이 보장되는가?
답:
1. synchronized:
공정 vs 비공정:
문제:
해결:
synchronized 의 단점 3가지:
1. 무한 대기
- 타임아웃 설정 불가
- 락 받을 때까지 무한
2. 인터럽트 불가
- 대기 중 외부에서 못 깨움
- interrupt() 안 통함
3. 공정성 보장 X
- 순서 미정의
- 기아 가능
무한 대기:
synchronized 진입 시 락 없으면
무한정 대기 (BLOCKED).
- 타임아웃 X
- "1초만 기다리고 포기" 불가
- 락 영원히 안 풀리면 영원히 대기
문제:
- 데드락 시 회복 불가
- 응답 지연
// synchronized 대기 중 인터럽트 안 통함
Thread t = new Thread(() -> {
synchronized (lock) { // 락 대기 (BLOCKED)
// ...
}
});
t.start();
t.interrupt(); // ❌ BLOCKED 는 인터럽트 안 됨
// 락 받을 때까지 계속 대기
// ReentrantLock 은 lockInterruptibly() 가능 (Phase 5)
공정성 X (앞 섹션):
- 순서 보장 안 됨
- 기아 가능
- 예측 불가
ReentrantLock(true) 로 해결 가능
// synchronized 한계
synchronized (lock) {
// 무한 대기, 인터럽트 X, 비공정
}
// ReentrantLock 극복 (Phase 5)
ReentrantLock lock = new ReentrantLock(true); // 공정
// 1. 타임아웃
if (lock.tryLock(5, TimeUnit.SECONDS)) { // 5초만
try { } finally { lock.unlock(); }
}
// 2. 인터럽트 가능
lock.lockInterruptibly(); // 인터럽트로 깰 수 있음
// 3. 공정성
new ReentrantLock(true); // FIFO
@Service
public class SynchronizedLimitations {
private final Object lock = new Object();
// ❌ synchronized 한계
public void processSync(Shipment shipment) {
synchronized (lock) {
// 1. 무한 대기 (타임아웃 X)
// 2. 인터럽트 X
// 3. 비공정
doProcess(shipment);
}
}
// ✓ ReentrantLock 극복 (Phase 5)
private final ReentrantLock reentrantLock = new ReentrantLock();
public boolean processWithTimeout(Shipment shipment) {
try {
// 1. 타임아웃
if (reentrantLock.tryLock(5, TimeUnit.SECONDS)) {
try {
doProcess(shipment);
return true;
} finally {
reentrantLock.unlock();
}
}
return false; // 5초 내 못 얻음
} catch (InterruptedException e) {
// 2. 인터럽트 가능
Thread.currentThread().interrupt();
return false;
}
}
private void doProcess(Shipment s) { }
}
synchronized의 단점 3가지는?
답:
1. 무한 대기:
인터럽트 불가:
공정성 X:
극복:
| Q | 핵심 답변 |
|---|---|
| 모니터 락? | 모든 객체의 내장 락 |
| intrinsic lock? | 고유 락 (내재) |
| 락 획득? | 성공 RUNNABLE, 실패 BLOCKED |
| BLOCKED? | 락 대기 (Entry Set) |
| 락 반환? | 종료 시 자동 (예외 포함) |
| 100 스레드? | 1개 성공, 99개 BLOCKED |
| 공정성? | 보장 X (비공정) |
| 기아? | 특정 스레드 계속 못 받음 |
| 단점 3가지? | 무한 대기, 인터럽트 X, 비공정 |
| Mark Word? | 객체 헤더의 락 정보 |
Q1. 모니터 락? → 모든 객체 내장 락
Q2. intrinsic lock? → 고유 락
Q3. 모든 객체? → Object 상속
Q4. 모니터 구성? → 락 + 대기 집합
Q5. 객체 헤더? → Mark Word + Klass
Q6. Mark Word? → 락 정보
Q7. 락 상태 전이? → Biased→Lightweight→Heavyweight
Q8. Biased Lock? → 단일 스레드 최적화
Q9. 소유 스레드? → 현재 락 보유
Q10. 재진입 카운트? → 재획득 횟수
Q11. 진입 시? → monitorenter
Q12. 성공? → RUNNABLE
Q13. 실패? → BLOCKED
Q14. 종료 시? → monitorexit
Q15. 예외 시 반환? → 보장 (자동)
Q16. 자동 반환 장점? → unlock 불필요
Q17. Entry Set? → 락 대기 (BLOCKED)
Q18. Wait Set? → wait (WAITING)
Q19. BLOCKED 깨어남? → 락 반납
Q20. 인터럽트로 BLOCKED? → 안 깨어남
Q21. 100 스레드? → 1 성공, 99 BLOCKED
Q22. 직렬화? → 하나씩
Q23. 경합? → 대기 많음
Q24. 높은 경합 영향? → 처리량 ↓
Q25. 경합 줄이기? → 락 분리, Atomic
Q26. Lock Striping? → 분할 락
Q27. lock-free? → Atomic (CAS)
Q28. 컨텍스트 스위칭? → 경합 시 ↑
Q29. 범위 최소화? → 경합 ↓
Q30. 무상태? → 경합 X
Q31. 공정성? → 획득 순서 = 대기 순서
Q32. synchronized 공정? → 비공정
Q33. 비공정? → 새치기 가능
Q34. 기아? → 계속 못 받음
Q35. 공정 락? → ReentrantLock(true)
Q36. 공정 단점? → 성능 ↓
Q37. 비공정 장점? → 처리량 ↑
Q38. barging? → 새치기
Q39. FIFO? → 공정 락
Q40. 순서 중요 시? → 공정 락
Q41. 단점 1? → 무한 대기
Q42. 단점 2? → 인터럽트 X
Q43. 단점 3? → 공정성 X
Q44. 무한 대기 문제? → 데드락 회복 X
Q45. 타임아웃? → tryLock (Phase 5)
Q46. 인터럽트 가능? → lockInterruptibly
Q47. 공정성? → ReentrantLock(true)
Q48. 극복 도구? → ReentrantLock
Q49. synchronized 장점? → 간단, 자동 반환
Q50. 언제 synchronized? → 단순, 짧은 임계
50 / 50 → 모니터 락 마스터
45-49 → 거의 마스터
40-44 → 복습
< 40 → Unit 4.4 재학습
답:
답:
답:
답:
답:
1. 모니터 락
2. 경쟁과 공정성
3. 단점 3가지
이번 Unit에서 모니터 락을 봤다면, 다음은 volatile (★ 마스터).
🚀 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 완주
✅ Phase 1 — 동시성의 기초 (4 Unit)
✅ Phase 2 — 4분면 매트릭스 (3 Unit)
✅ Phase 3 — 스레드 다루기 (5 Unit)
🚀 Phase 4 — synchronized & volatile (4/5 진행) ★ 1차 정점
총: 16/35 Unit