F-LAB JAVA · 4주차 · Phase 6 · 스레드 간 협력
🏆 Phase 6 완주 — 스레드 협력 마스터
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
yield()는 현재 스레드가 CPU 를 다른 스레드에게 양보하겠다는 "힌트" 를 스케줄러에 전달하는 메서드로, RUNNABLE 상태를 유지하며 강제력이 없다.
Thread.yield()는 현재 스레드를 실행 가능 (Ready) 상태로 두어 다른 같은 우선순위 스레드에게 실행 기회를 줄 수 있지만, 상태는 여전히 RUNNABLE 이다 (sleep·wait 와 달리 대기 상태로 가지 않음).
yield 는 단순 힌트일 뿐 이라 스케줄러가 무시할 수 있고, 양보 직후 같은 스레드가 다시 선택될 수도 있다 (보장 없음).
상태로 비교하면 yield 는 RUNNABLE 유지, sleep(ms) 는 TIMED_WAITING, wait() 는 WAITING 으로 가며, sleep·wait 은 명확한 대기인 반면 yield 는 즉시 복귀 가능한 양보다.
동작이 플랫폼·JVM 에 따라 다르고 효과를 보장할 수 없어 실무에서는 거의 사용하지 않으며, 양보가 필요하면 더 명확한 도구 (sleep, Lock, 적절한 동기화) 를 쓴다.
yield = 줄에서 "먼저 가세요" 양보:
yield():
- "급한 분 먼저 가세요" (양보 힌트)
- 하지만 줄에서 나간 건 아님 (RUNNABLE)
- 양보 후 다시 내 차례일 수도 (보장 X)
상태 비교:
yield: 줄에 그대로 (RUNNABLE) — 잠깐 양보
sleep: "30분 후 올게요" (TIMED_WAITING) — 자리 비움
wait: "부르면 올게요" (WAITING) — 대기실로
힌트일 뿐:
- 직원이 "아니에요 그냥 가세요" 할 수도
- 양보 효과 없을 수도 (스케줄러 무시)
실무:
- 너무 불확실 (보장 X)
- 더 명확한 방법 (sleep, 동기화)
→ yield = CPU 양보 힌트 (RUNNABLE 유지, 보장 X), 실무 드묾.
1. yield()의 동작
2. RUNNABLE 유지
3. 단순 힌트 (보장 X)
4. sleep / wait / yield 상태 비교
5. 양보 후 즉시 재실행
6. yield의 사용 사례
7. 실무에서 거의 안 쓰는 이유
8. Phase 6 완주 정리
9. 면접 + 자기 점검 + Phase 6 졸업 시험
yield():
현재 스레드가 CPU 를 양보하겠다는
힌트를 스케줄러에 전달.
특징:
- 양보 힌트 (강제 X)
- RUNNABLE 유지
- static 메서드
// Thread 의 static 메서드
public static void yield();
// 사용
Thread.yield(); // 현재 스레드 양보 힌트
yield 동작:
현재 스레드:
- 실행 가능 (Ready) 상태로
- 다른 스레드에 기회
- 단, RUNNABLE 유지
스케줄러:
- 다른 스레드 선택 가능
- 또는 같은 스레드 재선택
Thread t = new Thread(() -> {
for (int i = 0; i < 100; i++) {
doWork(i);
Thread.yield(); // 매 반복 양보 힌트
}
});
t.start();
양보 (yield) 의미:
"다른 스레드 먼저 실행해도 돼"
- CPU 점유 포기 (잠깐)
- 협조적 멀티태스킹
하지만:
- 힌트일 뿐
- 스케줄러 결정
- 효과 불확실
public class YieldBasics {
// yield 사용 (드문 경우)
public void busyTask() {
while (!isReady()) {
// 바쁜 대기 중 양보
Thread.yield(); // 다른 스레드에 기회
// 단, 효과 불확실
}
process();
}
// 실무: 보통 wait/notify, sleep 사용
private boolean isReady() { return false; }
private void process() { }
}
yield()의 동작은?
답:
1. 정의:
메서드:
동작:
양보:
yield 의 상태:
RUNNABLE 유지!
- sleep: TIMED_WAITING
- wait: WAITING
- yield: RUNNABLE (그대로)
→ 대기 상태로 안 감
yield 가 RUNNABLE 인 이유:
대기가 아니라 양보.
- 실행 가능한 상태 유지
- 즉시 다시 실행 가능
- 단지 다른 스레드에 기회
대조:
- sleep/wait: 대기 (실행 불가)
- yield: 실행 가능 (양보만)
Thread t = new Thread(() -> {
while (true) {
Thread.yield(); // 양보
}
});
t.start();
Thread.sleep(100);
System.out.println(t.getState()); // RUNNABLE
// yield 중에도 RUNNABLE (대기 X)
RUNNABLE 의 의미:
yield 후:
- 즉시 다시 실행 가능
- 스케줄러가 같은 스레드 선택 가능
→ 양보했지만 바로 돌아올 수 있음
→ 효과 약함
상태 비교:
yield:
RUNNABLE → (양보) → RUNNABLE
(대기 안 함)
sleep:
RUNNABLE → TIMED_WAITING → RUNNABLE
(시간 대기)
wait:
RUNNABLE → WAITING → RUNNABLE
(notify 대기)
public class YieldStateExample {
public void demonstrateStates() throws InterruptedException {
// yield → RUNNABLE
Thread yieldThread = new Thread(() -> {
while (true) {
Thread.yield(); // RUNNABLE 유지
}
});
yieldThread.setDaemon(true);
yieldThread.start();
Thread.sleep(100);
log.info("Yield state: {}", yieldThread.getState()); // RUNNABLE
// sleep → TIMED_WAITING
Thread sleepThread = new Thread(() -> {
try { Thread.sleep(10000); } catch (Exception e) {}
});
sleepThread.start();
Thread.sleep(100);
log.info("Sleep state: {}", sleepThread.getState()); // TIMED_WAITING
}
}
yield()가 RUNNABLE을 유지하는 이유는?
답:
1. RUNNABLE 유지:
이유:
vs sleep/wait:
효과:
yield 는 단순 힌트:
스케줄러에 "양보하고 싶다" 전달.
- 강제력 X
- 스케줄러가 무시 가능
- 효과 보장 X
스케줄러 결정:
yield 호출 시:
- 다른 스레드 선택 (양보 성공)
- 또는 같은 스레드 재선택 (양보 무시)
스케줄러 마음:
- OS/JVM 구현
- 우선순위
- 부하
플랫폼 의존:
yield 동작이 다름:
- OS 마다
- JVM 마다
- 버전마다
예:
- 어떤 플랫폼: 효과 있음
- 다른 플랫폼: 무시 (no-op)
→ 일관성 없음
yield 가 보장 안 하는 것:
✗ 다른 스레드가 실행됨
✗ 양보 시간
✗ 양보 순서
✗ 일관된 동작
→ 단지 힌트
yield javadoc 요지:
"스케줄러에 대한 힌트"
"거의 사용하지 않음"
"디버깅/테스트 외 유용성 낮음"
"동시성 제어에 의존하면 안 됨"
→ 공식적으로도 주의
public class YieldHintExample {
// ❌ yield 에 의존 (위험)
private boolean ready = false;
public void unreliableWait() {
while (!ready) {
Thread.yield(); // 효과 불확실
// 어떤 플랫폼: 양보, 다른: 무시
// 동기화 보장 X
}
process();
}
// ✓ 명확한 방법 (권장)
private final Object lock = new Object();
public void reliableWait() throws InterruptedException {
synchronized (lock) {
while (!ready) {
lock.wait(); // 확실한 대기
}
}
process();
}
private void process() { }
}
yield()가 단순 힌트인 이유는?
답:
1. 힌트:
스케줄러 결정:
플랫폼 의존:
보장 X:
| 항목 | yield() | sleep(ms) | wait() |
|---|---|---|---|
| 상태 | RUNNABLE | TIMED_WAITING | WAITING |
| 락 | 무관 | 유지 | 반납 |
| 클래스 | Thread | Thread | Object |
| synchronized | 무관 | 무관 | 필요 |
| 깨우기 | 즉시 가능 | 시간 | notify |
| 보장 | X (힌트) | O (시간) | O (notify) |
상태 차이:
yield:
- RUNNABLE
- 실행 가능 (양보)
sleep:
- TIMED_WAITING
- 시간 동안 대기
wait:
- WAITING
- notify 까지 대기
락 차이:
yield:
- 락 무관 (락 안 다룸)
- synchronized 안에서 yield 시 락 유지
sleep:
- 락 유지 (반납 X)
wait:
- 락 반납
용도:
yield:
- CPU 양보 (드묾)
- 효과 불확실
sleep:
- 시간 지연
- 폴링 간격
wait:
- 조건 대기
- 협력 (notify)
세 메서드 상태:
yield():
RUNNABLE ─양보─→ RUNNABLE
(즉시 복귀 가능)
sleep(1000):
RUNNABLE → TIMED_WAITING → RUNNABLE
(1초 후)
wait():
RUNNABLE → WAITING → RUNNABLE
(notify 후, 락 재획득)
public class ThreeMethodsComparison {
private final Object lock = new Object();
private boolean condition = false;
// yield — 양보 (RUNNABLE, 드묾)
public void withYield() {
while (!condition) {
Thread.yield(); // RUNNABLE, 효과 불확실
}
}
// sleep — 시간 지연 (TIMED_WAITING, 락 유지)
public void withSleep() throws InterruptedException {
while (!condition) {
Thread.sleep(100); // TIMED_WAITING, 폴링
}
}
// wait — 조건 대기 (WAITING, 락 반납, 권장)
public void withWait() throws InterruptedException {
synchronized (lock) {
while (!condition) {
lock.wait(); // WAITING, 락 반납, 효율적
}
}
}
public void setCondition() {
synchronized (lock) {
condition = true;
lock.notifyAll();
}
}
}
sleep / wait / yield의 상태 비교는?
답:
1. 상태:
락:
클래스:
보장:
yield 후 즉시 재실행:
yield 했어도:
- RUNNABLE 유지
- 스케줄러가 같은 스레드 재선택 가능
- 양보 효과 없음
→ "양보했는데 또 나"
즉시 재실행 가능 이유:
yield 는 RUNNABLE 유지:
- 대기 상태 X
- 실행 가능 풀에 그대로
- 스케줄러가 다시 선택 가능
특히:
- 다른 스레드 없으면
- 같은 스레드 재선택
시나리오:
스레드 A: yield()
- 스케줄러 결정:
a) 다른 스레드 (B) 선택 → 양보 성공
b) A 재선택 → 양보 실패 (즉시 복귀)
→ b) 가능 (보장 X)
코어 수와 yield:
단일 코어:
- yield 효과 있을 수 있음
- 다른 스레드 기회
멀티 코어:
- 이미 병렬 실행
- yield 효과 적음
- 양보 의미 약함
public class YieldImmediateReturn {
private final AtomicInteger yieldCount = new AtomicInteger();
private final AtomicInteger sameThreadCount = new AtomicInteger();
public void demonstrateUnreliable() {
Thread t = new Thread(() -> {
String name = Thread.currentThread().getName();
for (int i = 0; i < 1000; i++) {
Thread.yield(); // 양보
// 양보했어도 같은 스레드가 계속 실행 가능
yieldCount.incrementAndGet();
}
});
t.start();
// yield 효과 측정 어려움 (불확실)
// → 실무 의존 X
}
}
양보 후 즉시 다시 실행될 수 있는 이유는?
답:
1. 즉시 재실행:
이유:
시나리오:
코어:
yield 사용 사례 (드묾):
1. 테스트
- 경쟁 조건 유발
- 타이밍 변화
2. 디버깅
- 스레드 전환 유도
3. busy-wait 완화 (제한적)
- spin lock 에서
4. 협조적 멀티태스킹 (구식)
// 테스트 — 경쟁 조건 유발
public void testRaceCondition() {
// yield 로 스레드 전환 유도
Thread t1 = new Thread(() -> {
read();
Thread.yield(); // 전환 유도 (경쟁 조건 노출)
write();
});
// 버그 재현에 도움
}
// spin lock 에서 (제한적)
public class SpinLock {
private final AtomicBoolean locked = new AtomicBoolean();
public void lock() {
while (!locked.compareAndSet(false, true)) {
Thread.yield(); // 짧은 양보 (busy 완화)
}
}
public void unlock() {
locked.set(false);
}
}
// 하지만 현대는 더 나은 도구
// Java 9+ — Thread.onSpinWait (yield 대체)
public void spinWait() {
while (!ready) {
Thread.onSpinWait(); // spin 힌트 (CPU 최적화)
// yield 보다 적합 (spin loop)
}
}
// onSpinWait: CPU 에 spin 중 알림 (전력/성능)
yield 사용 자제:
대부분의 경우:
- 더 나은 도구 존재
- wait/notify (조건)
- sleep (지연)
- Lock (동기화)
- onSpinWait (spin)
yield:
- 효과 불확실
- 의존 X
public class YieldUseCases {
// ❌ yield 의존 (지양)
public void avoid() {
while (!ready) {
Thread.yield(); // 불확실
}
}
// ✓ 대안 1 — wait/notify (조건)
public void useWait() throws InterruptedException {
synchronized (lock) {
while (!ready) {
lock.wait();
}
}
}
// ✓ 대안 2 — onSpinWait (spin, Java 9+)
public void useOnSpinWait() {
while (!ready) {
Thread.onSpinWait(); // spin 최적화
}
}
// ✓ 대안 3 — BlockingQueue (생산자-소비자)
private final BlockingQueue<Shipment> queue = new LinkedBlockingQueue<>();
public Shipment useQueue() throws InterruptedException {
return queue.take(); // 효율적 대기
}
private volatile boolean ready = false;
private final Object lock = new Object();
}
yield의 사용 사례는?
답:
1. 드문 사례:
테스트:
onSpinWait:
자제:
yield 실무 안 쓰는 이유:
1. 효과 보장 X
- 힌트일 뿐
- 스케줄러 무시 가능
2. 플랫폼 의존
- 동작 다름
- 일관성 없음
3. 더 나은 도구
- wait/notify, sleep
- Lock, BlockingQueue
4. busy-wait
- CPU 여전히 사용
효과 불확실:
같은 코드:
- 환경 A: 양보됨
- 환경 B: 무시됨
→ 예측 불가
→ 신뢰 X
yield 대안:
목적별:
조건 대기:
→ wait/notify, Condition
시간 지연:
→ sleep
동기화:
→ synchronized, Lock
생산자-소비자:
→ BlockingQueue
spin:
→ onSpinWait
yield 고려 (극히 드묾):
- 테스트에서 타이밍 변화
- 매우 특수한 성능 튜닝
- 레거시 코드
대부분:
- 안 씀
- 더 명확한 도구
현대적 관점:
동시성 제어 우선순위:
1. 고수준 (Executor, BlockingQueue)
2. 동기화 (Lock, synchronized)
3. 조건 (wait/notify, Condition)
4. 저수준 (LockSupport)
yield:
- 거의 안 씀
- 힌트 수준
@Service
public class WhyNotYield {
// 실무 패턴 (yield 대신)
// 1. 작업 처리 — BlockingQueue
private final BlockingQueue<Shipment> queue = new LinkedBlockingQueue<>();
public Shipment getNext() throws InterruptedException {
return queue.take(); // yield 아님
}
// 2. 조건 대기 — wait/notify
public void waitForCondition() throws InterruptedException {
synchronized (lock) {
while (!ready) {
lock.wait(); // yield 아님
}
}
}
// 3. 폴링 — sleep
public void poll() throws InterruptedException {
while (!isReady()) {
Thread.sleep(100); // yield 아님
}
}
// 4. 비동기 — CompletableFuture (Phase 8)
public CompletableFuture<Shipment> async(Long id) {
return CompletableFuture.supplyAsync(() -> repository.findById(id));
}
// yield 거의 안 씀
private final Object lock = new Object();
private volatile boolean ready;
private boolean isReady() { return ready; }
}
실무에서 거의 안 쓰는 이유는?
답:
1. 효과 보장 X:
플랫폼 의존:
더 나은 도구:
현대:
Phase 6 — 스레드 간 협력
Unit 6.1 — 생산자-소비자 문제
- 버퍼 매개 협력
- 차/빔 대기
- BlockingQueue
Unit 6.2 — wait()과 notify()
- 락 반납 + 대기
- while 재확인
- wait vs sleep
Unit 6.3 — 인터럽트 메커니즘
- 협력적 중단
- 플래그 + 복원
- 협력적 종료
Unit 6.4 — yield()
- 양보 힌트
- RUNNABLE 유지
- 거의 안 씀
스레드 협력:
1. 협력 문제
- 생산자-소비자
2. 조건 동기화
- wait/notify
3. 중단 협력
- 인터럽트
4. CPU 양보
- yield (드묾)
핵심:
- 스레드 간 협력
- 효율적 대기/통지
Phase 6 → Phase 7:
- 저수준 협력 → 고수준 프레임워크
Phase 7 — Executor 프레임워크 (★ 2차 정점):
- 스레드 풀
- ExecutorService
- Future/Callable
- ThreadPoolExecutor (★ 마스터)
Phase 6 핵심 통찰 5가지:
1. 생산자-소비자
- 멀티스레드 핵심 문제
2. wait/notify
- 락 반납, while 재확인
3. 인터럽트
- 협력적, 플래그 복원
4. yield
- 힌트, 거의 안 씀
5. 고수준 권장
- BlockingQueue
- 직접 wait/notify 보다
✅ Phase 1 — 동시성의 기초 (4 Unit)
✅ Phase 2 — 4분면 매트릭스 (3 Unit)
✅ Phase 3 — 스레드 다루기 (5 Unit)
✅ Phase 4 — synchronized & volatile (5 Unit) ★ 1차 정점
✅ Phase 5 — Lock 도구 (4 Unit)
✅ Phase 6 — 스레드 협력 (4 Unit) ← 완주
⏭ Phase 7 — Executor (7 Unit) ★ 2차 정점
⏭ Phase 8 — 고급 비동기 (3 Unit)
총: 25/35 Unit (Phase 6 완주, 약 71%)
Phase 6의 종합은?
답:
1. 4개 Unit:
큰 그림:
핵심:
| Q | 핵심 답변 |
|---|---|
| yield()? | CPU 양보 힌트 |
| yield 상태? | RUNNABLE |
| 보장? | 없음 (힌트) |
| sleep/wait/yield 상태? | TIMED_WAITING/WAITING/RUNNABLE |
| 즉시 재실행? | RUNNABLE 유지 |
| 사용 사례? | 테스트, 드묾 |
| 안 쓰는 이유? | 효과 불확실 |
| onSpinWait? | spin 최적화 (Java 9+) |
| 대안? | wait/sleep/Lock |
| Phase 6 핵심? | 스레드 협력 |
Q1. 생산자-소비자? → 버퍼 매개 협력
Q2. 생산자? → 생성 → 버퍼
Q3. 소비자? → 버퍼 → 처리
Q4. 버퍼 가득? → 생산자 대기
Q5. 버퍼 빔? → 소비자 대기
Q6. 사례? → MQ, 로깅, 작업 큐
Q7. 일반 락 한계? → 바쁜 대기
Q8. 바쁜 대기? → CPU 낭비
Q9. BlockingQueue? → put/take 자동 대기
Q10. 백프레셔? → 유한 버퍼
Q11. ArrayBlockingQueue? → 고정 크기
Q12. SynchronousQueue? → 크기 0
Q13. wait()? → 락 반납 + WAITING
Q14. synchronized 안? → 모니터 락
Q15. 밖에서 wait? → IllegalMonitorStateException
Q16. notify()? → 하나 깸
Q17. notifyAll()? → 모두 깸
Q18. while vs if? → 재확인 (while)
Q19. spurious wakeup? → 가짜 깨어남
Q20. wait vs sleep? → 락 반납 vs 유지
Q21. wait 클래스? → Object
Q22. notify 위험? → 잘못된 스레드
Q23. notifyAll 안전? → 모두 확인
Q24. lost notification? → 락 없으면
Q25. Condition? → 정교한 통지
Q26. 인터럽트? → 중단 요청 (협력적)
Q27. 강제 종료? → 아니다
Q28. interrupt()? → 플래그 설정
Q29. isInterrupted()? → 확인 (변경 X)
Q30. interrupted()? → 확인 + 리셋 (static)
Q31. InterruptedException 플래그? → 리셋
Q32. 복원? → interrupt() 재호출
Q33. 무시? → 위험 (안 멈춤)
Q34. 협력적 종료? → 플래그 확인 + 정리
Q35. 즉각 종료? → while (!interrupted())
Q36. stop()? → deprecated (위험)
Q37. BLOCKED 인터럽트? → 안 깸
Q38. WAITING 인터럽트? → 예외
Q39. yield()? → 양보 힌트
Q40. yield 상태? → RUNNABLE
Q41. yield 보장? → 없음
Q42. 즉시 재실행? → RUNNABLE 유지
Q43. yield vs sleep? → RUNNABLE vs TIMED_WAITING
Q44. yield 안 쓰는 이유? → 효과 불확실
Q45. onSpinWait? → spin 최적화
Q46. 양보 대안? → wait/sleep/Lock
Q47. Phase 6 주제? → 스레드 협력
Q48. 효율적 대기? → wait/BlockingQueue
Q49. 협력 핵심? → 생산자-소비자
Q50. 고수준 권장? → BlockingQueue
50 / 50 → Phase 6 마스터
45-49 → 거의 마스터
40-44 → 복습
< 40 → Unit 6.1 ~ 6.4 재학습
답:
답:
답:
답:
답:
1. yield()
2. 상태 비교
3. 실무
🚀 Phase 6 — 스레드 간 협력
✅ Unit 6.1 생산자-소비자 문제
✅ Unit 6.2 wait()과 notify()
✅ Unit 6.3 인터럽트 메커니즘
✅ Unit 6.4 yield() ← 여기, Phase 6 완주
→ 협력 문제 + 조건 동기화 + 중단 + 양보
→ 스레드 간 협력 완성
Phase 7 — Executor 프레임워크 (7 Unit)
Unit 7.1 — 스레드 풀의 필요성
Unit 7.2 — Executor와 ExecutorService
Unit 7.3 — Future와 Callable
Unit 7.4 — ThreadPoolExecutor 내부 (★ 마스터)
Unit 7.5 — 스레드 풀 종류
Unit 7.6 — 작업 큐와 거부 정책
Unit 7.7 — 스레드 풀 종료
✅ Phase 1~6 (25 Unit, 1차 정점 완료)
⏭ Phase 7 — Executor (7 Unit) ★ 2차 정점
총: 25/35 Unit (약 71%)