F-LAB JAVA · 4주차 · Phase 6 · 스레드 간 협력
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
wait()은 synchronized 블록 안에서 호출되어, 현재 스레드가 보유한 모니터 락을 반납하고 WAITING 상태로 대기하다가, 다른 스레드의notify()/notifyAll()로 깨어나 락을 재획득한 뒤 진행한다.
wait/notify 는 모니터 락이 필요 하므로 반드시 synchronized 블록 안에서 호출해야 한다 (아니면 IllegalMonitorStateException).
notify()는 대기 중인 스레드 하나를 깨우고,notifyAll()은 모두 깨우며, 일반적으로 notifyAll 이 안전하다 (깨울 스레드를 잘못 선택할 위험 없음).
조건 확인은 반드시if가 아닌while로 해야 한다 — wait 에서 깨어났어도 조건이 여전히 만족되지 않을 수 있고 (다른 스레드가 먼저 처리), spurious wakeup (가짜 깨어남) 으로 notify 없이 깨어날 수도 있기 때문이다.
wait 는 락을 반납 하지만sleep은 락을 유지 한다는 점이 핵심 차이다.
wait/notify = 병원 대기실:
wait() = 진료실 앞에서 대기:
- 차례 아니면 대기 (WAITING)
- 자리 양보 (락 반납)
- 호출될 때까지
notify() = 한 명 호출:
- "다음 분 들어오세요" (1명)
notifyAll() = 모두 호출:
- "모두 다시 확인하세요" (전부)
- 각자 자기 차례인지 재확인
while 재확인 (if 안 되는 이유):
- 호출받아 일어났는데
- 이미 다른 사람이 들어감 (조건 안 됨)
- 다시 앉아서 대기 (while 로 재확인)
- if 면 그냥 진행 (잘못! 차례 아닌데)
spurious wakeup:
- 호출 안 했는데 일어남 (가짜)
- while 로 재확인하면 안전
wait vs sleep:
- wait: 자리 양보 (락 반납)
- sleep: 자리 지킴 (락 유지)
→ wait = 락 반납 + 대기, notify = 깨움, while 로 조건 재확인 필수.
1. wait()의 동작
2. synchronized 안에서만
3. notify()와 notifyAll()
4. while vs if (조건 재확인)
5. spurious wakeup
6. wait vs sleep
7. notify vs notifyAll 선택
8. 생산자-소비자 구현
9. 면접 + 자기 점검
wait():
현재 스레드가 보유한 모니터 락을 반납하고
WAITING 상태로 대기.
동작:
1. 락 반납
2. WAITING 상태
3. notify/notifyAll 까지 대기
4. 깨어나면 락 재획득
wait() 흐름:
스레드:
synchronized 진입 (락 보유)
↓ wait()
락 반납 + WAITING
↓ (다른 스레드 notify)
깨어남 (대기)
↓ 락 재획득
RUNNABLE (synchronized 안 재개)
synchronized (lock) {
while (!condition) {
lock.wait(); // 락 반납 + 대기
// notify 시 깨어남 + 락 재획득
}
// 조건 충족 후 진행
}
// 무한 대기
void wait() throws InterruptedException;
// 타임아웃
void wait(long timeout) throws InterruptedException;
void wait(long timeout, int nanos) throws InterruptedException;
// 모두 InterruptedException
// Object 의 메서드
wait 후 락 재획득:
notify 로 깨어나도:
- 즉시 진행 X
- 락 재획득 필요
- 다른 스레드가 락 보유 중일 수 있음
락 획득 후:
- synchronized 안 재개
@Service
public class WaitExample {
private final Object lock = new Object();
private boolean dataReady = false;
private Shipment data;
// 대기 (소비자)
public Shipment waitForData() throws InterruptedException {
synchronized (lock) {
while (!dataReady) {
lock.wait(); // 락 반납 + 대기
}
dataReady = false;
return data; // 데이터 받음
}
}
// 통지 (생산자)
public void provideData(Shipment shipment) {
synchronized (lock) {
data = shipment;
dataReady = true;
lock.notify(); // 대기 스레드 깨움
}
}
}
wait()의 동작은?
답:
1. 정의:
흐름:
종류:
재획득:
wait/notify 는 synchronized 안에서만:
이유:
- wait/notify 는 모니터 락 사용
- 락 보유 상태에서만 호출
- 아니면 IllegalMonitorStateException
// ❌ synchronized 밖에서 wait
public void wrong() throws InterruptedException {
lock.wait(); // IllegalMonitorStateException
// 락 없이 wait 호출
}
// ✓ synchronized 안에서
public void correct() throws InterruptedException {
synchronized (lock) {
lock.wait(); // OK (락 보유)
}
}
왜 락이 필요한가:
wait/notify 는 조건 동기화.
- 조건 확인 (공유 상태)
- 대기/통지
- 원자적이어야
락 없으면:
- 조건 확인과 wait 사이 경쟁
- lost notification
→ 락으로 보호
// wait/notify 는 같은 락 객체에
synchronized (lock) {
lock.wait(); // lock 의 모니터
}
synchronized (lock) {
lock.notify(); // 같은 lock 의 모니터
}
// 다른 객체면:
synchronized (lockA) {
lockB.wait(); // ❌ lockB 락 없음 → 예외
}
lost notification (락 없으면):
// 락 없는 가상 시나리오
if (!condition) { // 확인
// ← 여기서 다른 스레드 notify
wait(); // 놓침 (이미 notify 됨)
}
→ 영원히 대기 (신호 놓침)
락으로:
- 확인 + wait 원자적
- notify 도 락 안
- 안전
@Service
public class SynchronizedRequirement {
private final Object lock = new Object();
private boolean ready = false;
// ✓ synchronized 안에서 wait
public void consumer() throws InterruptedException {
synchronized (lock) { // 락 필요
while (!ready) {
lock.wait(); // 같은 lock
}
process();
}
}
// ✓ synchronized 안에서 notify
public void producer() {
synchronized (lock) { // 같은 lock
ready = true;
lock.notify();
}
}
// ❌ 잘못된 예
public void wrong() throws InterruptedException {
// lock.wait(); // synchronized 밖 → 예외
}
private void process() { }
}
wait()가 synchronized 안에서만 가능한 이유는?
답:
1. 모니터 락:
예외:
이유:
같은 객체:
notify():
대기 중인 스레드 하나를 깨움.
특징:
- 임의의 하나 (선택 불가)
- 나머지는 계속 대기
- synchronized 안에서
notifyAll():
대기 중인 모든 스레드를 깨움.
특징:
- 모두 깨움
- 각자 조건 재확인
- 하나만 진행 (락 경쟁)
synchronized (lock) {
ready = true;
lock.notify(); // 하나만 깨움
// 또는
lock.notifyAll(); // 모두 깨움
}
notify() 의 위험:
깨울 스레드를 선택 X (임의).
문제:
- 잘못된 스레드 깨움
- 조건 안 맞는 스레드
- 다시 wait
- 진짜 처리할 스레드는 대기
→ lost wakeup 가능
notifyAll() 안전:
모두 깨움 → 각자 확인:
- 조건 맞는 스레드 진행
- 아닌 스레드 다시 wait
단점:
- 모두 깨어남 (오버헤드)
- 락 경쟁
하지만 안전 (lost wakeup X)
@Service
public class NotifyExample {
private final Object lock = new Object();
private final Queue<Shipment> queue = new LinkedList<>();
public Shipment consume() throws InterruptedException {
synchronized (lock) {
while (queue.isEmpty()) {
lock.wait(); // 대기
}
return queue.poll();
}
}
public void produce(Shipment shipment) {
synchronized (lock) {
queue.offer(shipment);
lock.notifyAll(); // 모두 깨움 (안전)
// notify() 면 잘못된 스레드 깨울 수도
}
}
// notifyAll: 여러 소비자가 모두 깨어나
// 큐에서 하나씩 가져감 (나머지 다시 wait)
}
notify()와 notifyAll()의 차이는?
답:
1. notify():
notifyAll():
notify 위험:
notifyAll 안전:
조건 확인은 while:
✓ while (!condition) { wait(); }
❌ if (!condition) { wait(); }
이유:
- 깨어나도 조건 재확인
- spurious wakeup 대비
- 다른 스레드가 먼저 처리 가능
// ❌ if (위험)
synchronized (lock) {
if (queue.isEmpty()) {
lock.wait();
}
return queue.poll(); // ★ 위험
// 깨어났을 때 큐가 다시 비었을 수 있음
// → null 반환 또는 예외
}
// ✓ while (안전)
synchronized (lock) {
while (queue.isEmpty()) {
lock.wait();
}
return queue.poll(); // 조건 재확인됨 (안전)
}
깨어나도 조건 안 맞는 경우:
1. 다른 스레드가 먼저 처리:
- notifyAll 로 여러 깨어남
- 하나가 큐 비움
- 나머지는 빈 큐
2. spurious wakeup:
- notify 없이 깨어남
→ 깨어나면 조건 재확인 (while)
while vs if:
notifyAll 로 소비자 A, B 깨어남:
if (위험):
A: 깨어남 → poll (데이터 1개 가져감)
B: 깨어남 → poll (큐 비었는데!) → null/예외
while (안전):
A: 깨어남 → while 재확인 → 데이터 있음 → poll
B: 깨어남 → while 재확인 → 큐 비었음 → 다시 wait
// wait 표준 패턴 (항상 while)
synchronized (lock) {
while (!조건) { // while 필수
lock.wait();
}
// 조건 충족 (재확인됨)
실제_작업();
}
@Service
public class WhileVsIfExample {
private final Object lock = new Object();
private final Queue<Shipment> queue = new LinkedList<>();
// ✓ while (안전)
public Shipment consumeSafe() throws InterruptedException {
synchronized (lock) {
while (queue.isEmpty()) { // while
lock.wait();
// 깨어나도 다시 확인
}
return queue.poll(); // 안전 (조건 보장)
}
}
// ❌ if (위험)
public Shipment consumeUnsafe() throws InterruptedException {
synchronized (lock) {
if (queue.isEmpty()) { // if (위험)
lock.wait();
}
return queue.poll(); // 큐 비었을 수도 → null
}
}
public void produce(Shipment shipment) {
synchronized (lock) {
queue.offer(shipment);
lock.notifyAll();
}
}
}
while이 아닌 if를 쓰면 안 되는 이유는?
답:
1. while 필수:
if 위험:
이유:
표준:
spurious wakeup (가짜 깨어남):
notify 없이 wait 가 깨어나는 현상.
원인:
- OS/JVM 구현
- 하드웨어
- 드물지만 발생 가능
대비:
- while 로 조건 재확인
spurious wakeup 원인:
- OS 의 조건 변수 구현
- 시그널 처리
- 성능 최적화
→ 명세상 가능
→ 방어적 코딩 필요
// spurious wakeup 대비
synchronized (lock) {
while (!condition) { // while
lock.wait();
// spurious wakeup 으로 깨어나도
// while 로 조건 재확인
// 조건 안 맞으면 다시 wait
}
}
// if 면:
// spurious wakeup 시 조건 안 맞는데 진행 (버그)
spurious wakeup 은 보편적:
- Object.wait()
- Condition.await()
- LockSupport.park()
모두 spurious wakeup 가능
→ 모두 while 로 재확인
실무 권장:
조건 대기는 항상:
while (!조건) {
wait(); // 또는 await, park
}
- 절대 if 안 씀
- spurious wakeup + 다중 깨어남 대비
- 방어적 코딩
@Service
public class SpuriousWakeupHandling {
private final Object lock = new Object();
private boolean taskAvailable = false;
public void waitForTask() throws InterruptedException {
synchronized (lock) {
while (!taskAvailable) { // while (spurious 대비)
lock.wait();
// spurious wakeup 또는 notify
// 둘 다 while 로 재확인
}
taskAvailable = false;
processTask();
}
}
public void provideTask() {
synchronized (lock) {
taskAvailable = true;
lock.notifyAll();
}
}
private void processTask() { }
// 핵심: while 로 spurious wakeup + 다중 깨어남 모두 대비
}
spurious wakeup이란?
답:
1. 정의:
원인:
대비:
보편:
wait vs sleep 핵심:
wait():
- 락 반납 O
- 다른 스레드 락 획득 가능
sleep():
- 락 반납 X (유지)
- 다른 스레드 락 못 얻음
| 항목 | wait() | sleep() |
|---|---|---|
| 클래스 | Object | Thread |
| 락 반납 | O | X |
| 호출 위치 | synchronized 안 | 어디서나 |
| 깨우기 | notify/시간 | 시간/인터럽트 |
| 상태 | WAITING | TIMED_WAITING |
| 용도 | 조건 대기 | 시간 지연 |
// wait — 락 반납
synchronized (lock) {
lock.wait(); // 락 반납
// 다른 스레드가 lock 획득 가능
}
// sleep — 락 유지
synchronized (lock) {
Thread.sleep(1000); // 락 유지!
// 다른 스레드 lock 못 얻음 (1초 동안)
}
// ❌ synchronized 안 sleep (락 점유)
public void badSleep() throws InterruptedException {
synchronized (lock) {
Thread.sleep(5000); // 5초 락 점유!
// 다른 모든 스레드 BLOCKED
}
}
// 조건 대기는 wait 사용
public void goodWait() throws InterruptedException {
synchronized (lock) {
while (!condition) {
lock.wait(); // 락 반납 (다른 스레드 진행)
}
}
}
용도:
wait:
- 조건 대기
- 다른 스레드와 협력
- notify 로 깨움
sleep:
- 단순 시간 지연
- 폴링 간격
- 협력 X
@Service
public class WaitVsSleepExample {
private final Object lock = new Object();
private boolean ready = false;
// wait — 조건 대기 (락 반납)
public void waitForCondition() throws InterruptedException {
synchronized (lock) {
while (!ready) {
lock.wait(); // 락 반납 → 생산자 진행 가능
}
process();
}
}
// sleep — 시간 지연 (락 무관)
public void retryWithDelay() throws InterruptedException {
for (int i = 0; i < 3; i++) {
if (tryProcess()) return;
Thread.sleep(1000); // 1초 지연 (락 X)
}
}
// ❌ 잘못 — synchronized 안 sleep
public void wrong() throws InterruptedException {
synchronized (lock) {
Thread.sleep(5000); // 락 5초 점유 (나쁨)
}
}
private void process() { }
private boolean tryProcess() { return false; }
}
wait vs sleep의 차이는?
답:
1. 핵심:
클래스:
위치:
용도:
notify vs notifyAll:
notify:
- 하나만 깨움
- 효율적 (오버헤드 ↓)
- 위험 (잘못된 스레드)
notifyAll:
- 모두 깸
- 안전
- 오버헤드 (모두 깨어남)
notify 안전한 경우:
- 모든 대기 스레드가 동일 조건
- 깨운 스레드가 반드시 진행
- 단일 조건
예:
- 모든 소비자가 같은 큐 대기
- 아무나 처리 가능
→ notify OK
notifyAll 필요한 경우:
- 여러 조건 (다른 대기 이유)
- 깨운 스레드가 못 진행할 수 있음
- 안전 우선
예:
- 생산자/소비자 모두 같은 락 대기
- notify 면 잘못된 쪽 깨울 수도
→ notifyAll
// ❌ notify 위험 (생산자/소비자 같은 락)
synchronized (lock) {
// 소비자: 큐 비면 wait
// 생산자: 큐 가득 차면 wait
// 같은 lock
lock.notify(); // 잘못된 쪽 깨울 수 있음
// 소비자가 소비자 깨움 (둘 다 대기 지속)
}
// ✓ notifyAll (안전)
synchronized (lock) {
lock.notifyAll(); // 모두 깨움 → 맞는 쪽 진행
}
권장:
기본: notifyAll
- 안전
- 버그 적음
최적화: notify
- 조건 확실할 때만
- 성능 중요할 때
- 신중히
또는:
- Condition (여러 조건)
- BlockingQueue
@Service
public class NotifySelectionExample {
private final Object lock = new Object();
private final Queue<Shipment> queue = new LinkedList<>();
private final int capacity = 100;
// 생산자/소비자 같은 락 → notifyAll
public void produce(Shipment shipment) throws InterruptedException {
synchronized (lock) {
while (queue.size() >= capacity) {
lock.wait(); // 가득 차면 대기
}
queue.offer(shipment);
lock.notifyAll(); // 소비자 깨움 (안전)
}
}
public Shipment consume() throws InterruptedException {
synchronized (lock) {
while (queue.isEmpty()) {
lock.wait(); // 비면 대기
}
Shipment s = queue.poll();
lock.notifyAll(); // 생산자 깨움 (안전)
return s;
}
}
// 생산자/소비자 모두 같은 lock 대기
// → notifyAll 필수 (notify 면 같은 쪽 깨울 위험)
}
notify vs notifyAll 선택 기준은?
답:
1. notify:
notifyAll:
notify 안전:
권장:
public class ProducerConsumer<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
private final Object lock = new Object();
public ProducerConsumer(int capacity) {
this.capacity = capacity;
}
// 생산자
public void produce(T item) throws InterruptedException {
synchronized (lock) {
while (queue.size() >= capacity) {
lock.wait(); // 가득 차면 대기
}
queue.offer(item);
lock.notifyAll(); // 소비자 깨움
}
}
// 소비자
public T consume() throws InterruptedException {
synchronized (lock) {
while (queue.isEmpty()) {
lock.wait(); // 비면 대기
}
T item = queue.poll();
lock.notifyAll(); // 생산자 깨움
return item;
}
}
}
ProducerConsumer<Task> pc = new ProducerConsumer<>(10);
// 생산자 스레드
new Thread(() -> {
while (true) {
Task task = createTask();
pc.produce(task);
}
}).start();
// 소비자 스레드
new Thread(() -> {
while (true) {
Task task = pc.consume();
process(task);
}
}).start();
구현의 핵심:
1. synchronized (lock)
- 임계 영역 보호
2. while (!조건) wait()
- 조건 대기 (재확인)
3. notifyAll()
- 상대 깨움
4. 락 반납 (wait)
- 다른 스레드 진행
// wait/notify 직접 (복잡)
// 위 ProducerConsumer
// BlockingQueue (간단, 권장)
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(10);
queue.put(task); // 생산 (가득 차면 대기)
Task t = queue.take(); // 소비 (비면 대기)
// 내부적으로 wait/notify (또는 Condition)
// 직접 구현 불필요
// Condition — 정교한 통지 (Phase 5)
public class ProducerConsumerCondition<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public void produce(T item) throws InterruptedException {
lock.lock();
try {
while (queue.size() >= capacity) {
notFull.await(); // 가득 차면
}
queue.offer(item);
notEmpty.signal(); // 소비자만 (정확)
} finally {
lock.unlock();
}
}
public T consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 비면
}
T item = queue.poll();
notFull.signal(); // 생산자만 (정확)
return item;
} finally {
lock.unlock();
}
}
}
// signal: 정확한 통지 (notifyAll 오버헤드 ↓)
@Service
public class ShipmentProducerConsumer {
private final Queue<Shipment> queue = new LinkedList<>();
private final int capacity = 100;
private final Object lock = new Object();
// 생산자 (주문 접수)
public void submit(Shipment shipment) throws InterruptedException {
synchronized (lock) {
while (queue.size() >= capacity) {
lock.wait(); // 가득 차면 대기 (백프레셔)
}
queue.offer(shipment);
lock.notifyAll();
}
}
// 소비자 (워커)
public Shipment take() throws InterruptedException {
synchronized (lock) {
while (queue.isEmpty()) {
lock.wait(); // 비면 대기
}
Shipment shipment = queue.poll();
lock.notifyAll();
return shipment;
}
}
// 실무: BlockingQueue 권장 (검증됨)
}
생산자-소비자를 wait/notify로 구현하는 법은?
답:
1. 구조:
생산자:
소비자:
개선:
| Q | 핵심 답변 |
|---|---|
| wait()? | 락 반납 + 대기 |
| synchronized 안? | 모니터 락 필요 |
| notify()? | 하나 깸 |
| notifyAll()? | 모두 깸 |
| while vs if? | 조건 재확인 (while) |
| spurious wakeup? | notify 없이 깸 |
| wait vs sleep? | 락 반납 vs 유지 |
| notify 위험? | 잘못된 스레드 |
| 생산자-소비자? | while + wait + notifyAll |
| Condition? | 정확한 통지 |
답:
답:
답:
답:
답:
1. wait/notify
2. while과 spurious wakeup
3. wait vs sleep
이번 Unit에서 wait/notify 를 봤다면, 다음은 인터럽트 메커니즘.
🚀 Phase 6 — 스레드 간 협력
✅ Unit 6.1 생산자-소비자 문제
✅ Unit 6.2 wait()과 notify() ← 여기
⏭ Unit 6.3 인터럽트 메커니즘
⏭ Unit 6.4 yield()
✅ Phase 1~5 (21 Unit, 1차 정점 완료)
🚀 Phase 6 — 스레드 협력 (2/4 진행)
총: 23/35 Unit