F-LAB JAVA · 4주차 · Phase 3 · 스레드 만들고 다루기
🚀 Phase 3 시작 — 스레드 실전 진입
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
자바 스레드는
Thread.Stateenum 으로 정의된 6가지 상태 (NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED) 를 가지며, 이벤트에 따라 상태가 전이된다.
NEW (생성됨, 미시작) →start()→ RUNNABLE (실행 중 또는 실행 가능) → run 종료 → TERMINATED (종료).
RUNNABLE 에서 BLOCKED (synchronized 락 대기), WAITING (wait/join, 무한 대기), TIMED_WAITING (sleep/wait(ms), 시간 한정) 으로 빠질 수 있다.
BLOCKED 는 락 반납 시 깨어나고, WAITING 은 notify/join 대상 종료 시 깨어난다는 점이 다르다.
자바에는 별도의 RUNNING 상태가 없으며 (RUNNABLE 에 포함), 실제 CPU 실행 여부는 OS 스케줄러가 결정하므로getState()는 RUNNING 을 반환하지 않는다.
NEW: 회의 예약함 (아직 시작 X)
- Thread 객체 생성
RUNNABLE: 회의실 사용 중 또는 대기열
- 실행 중 (CPU 할당)
- 또는 실행 가능 (스케줄러 대기)
BLOCKED: 회의실 문이 잠김 (다른 사람 사용)
- synchronized 락 대기
- 락 풀리면 들어감
WAITING: "알려주면 올게요" (무한 대기)
- wait(): notify 까지
- join(): 대상 종료까지
TIMED_WAITING: "30분 후 올게요" (시간 한정)
- sleep(ms)
- wait(ms)
TERMINATED: 회의 끝남
- run 종료
→ 6가지 상태 = 스레드의 생애 주기.
1. 6가지 상태 개요
2. NEW와 RUNNABLE
3. RUNNABLE의 두 의미
4. BLOCKED
5. WAITING
6. TIMED_WAITING
7. TERMINATED와 상태 전이
8. getState()와 RUNNING
9. 면접 + 자기 점검
public enum Thread.State {
NEW, // 생성됨, 미시작
RUNNABLE, // 실행 중 또는 실행 가능
BLOCKED, // 모니터 락 대기
WAITING, // 무한 대기 (wait, join)
TIMED_WAITING, // 시간 한정 대기 (sleep, wait(ms))
TERMINATED // 종료
}
new Thread()
↓
┌───────┐
│ NEW │
└───┬───┘
│ start()
↓
┌───────────────┐
│ RUNNABLE │ ←──────────┐
└───┬───┬───┬───┘ │
│ │ │ │
락대기│ wait│ sleep │
│ join│ wait(ms) │
↓ ↓ ↓ │
┌────────┐ ┌────────┐ ┌──────────────┐
│BLOCKED │ │WAITING │ │TIMED_WAITING │
└────┬───┘ └───┬────┘ └──────┬───────┘
│ │ │
└─────────┴─────────────┘ (조건 충족 시 복귀)
│
run() 종료
↓
┌────────────┐
│ TERMINATED │
└────────────┘
| 상태 | 의미 | 진입 트리거 | 탈출 조건 |
|---|---|---|---|
| NEW | 생성, 미시작 | new Thread() | start() |
| RUNNABLE | 실행/실행 가능 | start() | 작업 종료, 대기 |
| BLOCKED | 락 대기 | synchronized | 락 획득 |
| WAITING | 무한 대기 | wait(), join() | notify, 종료 |
| TIMED_WAITING | 시간 대기 | sleep, wait(ms) | 시간 경과, notify |
| TERMINATED | 종료 | run 종료 | (최종) |
Thread t = new Thread(() -> {
// 작업
});
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE (보통)
// 다른 스레드에서 확인
Thread.sleep(100);
System.out.println(t.getState()); // 상태에 따라
public class ShipmentThreadMonitor {
public void monitorWorker(Thread worker) {
Thread.State state = worker.getState();
switch (state) {
case NEW -> log.info("Worker not started");
case RUNNABLE -> log.info("Worker running");
case BLOCKED -> log.warn("Worker blocked on lock!");
case WAITING -> log.info("Worker waiting");
case TIMED_WAITING -> log.info("Worker sleeping");
case TERMINATED -> log.info("Worker finished");
}
}
// 데드락 감지 (BLOCKED 가 많으면 의심)
public boolean detectPotentialDeadlock(List<Thread> workers) {
long blockedCount = workers.stream()
.filter(t -> t.getState() == Thread.State.BLOCKED)
.count();
return blockedCount > workers.size() / 2;
}
}
6가지 상태는?
답:
1. NEW: 생성, 미시작
2. RUNNABLE: 실행/실행 가능
3. BLOCKED: 락 대기
4. WAITING: 무한 대기 (wait/join)
5. TIMED_WAITING: 시간 대기 (sleep)
6. TERMINATED: 종료
확인: t.getState() → Thread.State enum
NEW:
Thread 객체가 생성되었지만
아직 start() 호출 전.
특징:
- 스레드 미시작
- OS 스레드 없음
- start() 로 RUNNABLE 전이
Thread t = new Thread(() -> System.out.println("Hello"));
// 이 시점 NEW
System.out.println(t.getState()); // NEW
// 아직 실행 X
// OS 레벨 스레드 없음
Thread t = new Thread(() -> {
// 작업
});
t.start(); // NEW → RUNNABLE
// OS 스레드 생성
// 실행 시작 (또는 실행 대기)
System.out.println(t.getState()); // RUNNABLE
// start() — 새 스레드
t.start();
// - NEW → RUNNABLE
// - 새 OS 스레드
// - run() 이 새 스레드에서 실행
// run() — 메서드 호출
t.run();
// - 상태 전이 X
// - 현재 스레드에서 실행
// - 새 스레드 X
// (Unit 3.2 에서 정밀)
NEW → RUNNABLE:
new Thread()
↓
[NEW]
Thread 객체만 (OS 스레드 X)
↓ start()
[RUNNABLE]
OS 스레드 생성
실행 또는 실행 가능
public class ShipmentWorkerLifecycle {
public void demonstrateLifecycle() {
// NEW
Thread worker = new Thread(() -> {
processShipments();
});
log.info("State: {}", worker.getState()); // NEW
// RUNNABLE
worker.start();
log.info("State: {}", worker.getState()); // RUNNABLE
// 주의: start() 두 번 호출하면 IllegalThreadStateException
// worker.start(); // ❌ 예외
}
private void processShipments() {
// 작업
}
}
NEW와 RUNNABLE은?
답:
1. NEW:
RUNNABLE:
전이:
start() vs run():
RUNNABLE 의 두 가지 의미:
1. 실제 실행 중 (Running)
- CPU 할당받아 실행
2. 실행 가능 (Ready)
- 실행 준비됨
- CPU 대기 (스케줄러 큐)
자바는 둘을 구분 안 함
→ 모두 RUNNABLE
RUNNABLE 이 포괄적인 이유:
실행 중 ↔ 실행 가능 전이는
OS 스케줄러가 매우 빠르게.
자바가 추적하기엔:
- 너무 빠른 전이
- OS 레벨 정보
- 의미 적음
→ 하나로 묶음 (RUNNABLE)
RUNNABLE 내부:
[RUNNABLE]
┌─────────────┐
│ Running │ ← CPU 실행 중
│ ↕ (스케줄러)│
│ Ready │ ← CPU 대기
└─────────────┘
자바 관점: 둘 다 RUNNABLE
OS 관점: 구분 (Running/Ready)
I/O 블로킹도 RUNNABLE:
네트워크/파일 I/O 대기 중에도
자바는 RUNNABLE 로 표시.
이유:
- I/O 대기는 OS 레벨
- 자바 스레드 상태는 JVM 레벨
- JVM 은 OS I/O 대기 모름
→ jstack 에서 RUNNABLE 이지만
실제론 I/O 대기일 수 있음
// jstack 결과 해석 주의
public void ioOperation() {
// 이 스레드는 RUNNABLE (자바)
byte[] data = socket.getInputStream().readAllBytes();
// 실제론 네트워크 I/O 대기
// 하지만 상태는 RUNNABLE
}
// 진단 시:
// - RUNNABLE 인데 CPU 안 쓰면
// - I/O 대기 의심
// - 스택 트레이스로 확인 (socketRead 등)
public class RunnableStateExample {
public void demonstrateRunnable() {
Thread worker = new Thread(() -> {
// CPU 작업 (Running)
long sum = computeSum();
// I/O 작업 (RUNNABLE 이지만 실제 대기)
Shipment s = repository.findById(1L);
// 둘 다 자바 관점 RUNNABLE
});
worker.start();
// 모니터링
log.info("State: {}", worker.getState()); // RUNNABLE
// CPU 실행 중인지 I/O 대기인지 구분 안 됨
}
private long computeSum() {
long sum = 0;
for (int i = 0; i < 1_000_000; i++) sum += i;
return sum;
}
}
RUNNABLE의 두 의미는?
답:
1. 두 의미:
구분 안 하는 이유:
I/O 대기:
진단:
BLOCKED:
synchronized 모니터 락을
획득하지 못해 대기하는 상태.
특징:
- 다른 스레드가 락 보유 중
- 락 반납까지 대기
- 락 획득 시 RUNNABLE 복귀
class Counter {
private int count = 0;
public synchronized void increment() {
count++; // 모니터 락 필요
}
}
// 스레드 A 가 increment() 실행 중 (락 보유)
// 스레드 B 가 increment() 호출
// → B 는 BLOCKED (락 대기)
BLOCKED 시나리오:
스레드 A:
synchronized 진입 (락 획득)
→ RUNNABLE
작업 중...
스레드 B:
synchronized 진입 시도
→ 락 없음
→ BLOCKED (대기)
스레드 A:
synchronized 종료 (락 반납)
스레드 B:
→ 락 획득
→ RUNNABLE 복귀
BLOCKED → RUNNABLE:
조건: 락이 반납됨
- 락 보유 스레드가 synchronized 종료
- BLOCKED 스레드 중 하나가 락 획득
- RUNNABLE 복귀
특징:
- 락 반납이 유일한 조건
- notify 등으로 깨우는 것 X (다른 메커니즘)
BLOCKED 가 많으면:
- 락 경합 심함
- 성능 저하
- 데드락 의심
진단:
- jstack 으로 BLOCKED 확인
- 어떤 락 대기하는지
- 데드락 감지
@Service
public class ShipmentSyncService {
private final Object lock = new Object();
private int processedCount = 0;
public void process(Shipment shipment) {
// 여러 스레드 동시 호출 시
synchronized (lock) { // 락 경합
// 한 스레드만 진입 (RUNNABLE)
// 나머지는 BLOCKED
processedCount++;
doWork(shipment);
}
}
private void doWork(Shipment s) {
// 작업 (이 동안 다른 스레드 BLOCKED)
}
}
// 모니터링: 많은 스레드가 BLOCKED 면
// → 락 경합 → synchronized 범위 줄이기 검토
BLOCKED는?
답:
1. 정의:
진입:
깨어남:
위험:
WAITING:
무한 대기 상태.
다른 스레드의 특정 동작까지 대기.
진입:
- Object.wait() (notify 까지)
- Thread.join() (대상 종료까지)
- LockSupport.park() (unpark 까지)
synchronized (lock) {
while (condition) {
lock.wait(); // RUNNABLE → WAITING
// 락 반납 + 대기
// notify/notifyAll 까지
}
}
Thread worker = new Thread(() -> doWork());
worker.start();
worker.join(); // 현재 스레드 RUNNABLE → WAITING
// worker 종료까지 대기
// worker 종료 시 RUNNABLE 복귀
WAITING → RUNNABLE:
wait() 로 진입:
- notify() / notifyAll() 호출 시
- (단, 락 재획득 필요)
join() 으로 진입:
- 대상 스레드 종료 시
park() 로 진입:
- unpark() 호출 시
- 인터럽트
BLOCKED vs WAITING 차이:
BLOCKED:
- 락 획득 실패 (synchronized 진입)
- 깨어남: 락 반납
- 수동적 (락 경쟁)
WAITING:
- 자발적 대기 (wait/join)
- 깨어남: notify / 대상 종료
- 능동적 (조건 대기)
핵심:
- BLOCKED: 락 때문에
- WAITING: 조건/이벤트 대기
WAITING 시나리오 (wait/notify):
스레드 A:
synchronized(lock) {
lock.wait(); → WAITING (락 반납)
}
스레드 B:
synchronized(lock) {
lock.notify(); // A 깨움
}
스레드 A:
→ 락 재획득 시도
→ RUNNABLE 복귀
@Service
public class ShipmentQueueProcessor {
private final Object lock = new Object();
private final Queue<Shipment> queue = new LinkedList<>();
// 소비자 — WAITING (큐 비면 대기)
public Shipment consume() throws InterruptedException {
synchronized (lock) {
while (queue.isEmpty()) {
lock.wait(); // WAITING (데이터 올 때까지)
}
return queue.poll();
}
}
// 생산자 — notify (대기 깨움)
public void produce(Shipment shipment) {
synchronized (lock) {
queue.offer(shipment);
lock.notify(); // WAITING 소비자 깨움
}
}
// join 예시
public void waitForWorker(Thread worker) throws InterruptedException {
worker.join(); // 현재 스레드 WAITING (worker 종료까지)
}
}
WAITING은?
답:
1. 정의:
진입:
깨어남:
vs BLOCKED:
TIMED_WAITING:
시간 제한이 있는 대기.
지정 시간 경과 또는 조건 충족 시 깨어남.
진입:
- Thread.sleep(ms)
- Object.wait(ms)
- Thread.join(ms)
- LockSupport.parkNanos(ns)
Thread.sleep(1000); // RUNNABLE → TIMED_WAITING
// 1초 대기
// 시간 경과 시 RUNNABLE 복귀
// 주의: sleep 은 락 반납 X (wait 와 다름)
WAITING vs TIMED_WAITING:
WAITING (무한):
- wait()
- join()
- park()
- 시간 제한 X
TIMED_WAITING (시간):
- wait(ms)
- sleep(ms)
- join(ms)
- parkNanos(ns)
- 시간 경과 시 자동 깨어남
핵심:
- 시간 제한 유무
sleep() vs wait() — 중요 차이:
sleep(ms):
- TIMED_WAITING
- 락 반납 X (보유 유지)
- Thread 의 static 메서드
- synchronized 불필요
wait(ms):
- TIMED_WAITING
- 락 반납 O
- Object 의 메서드
- synchronized 안에서만
핵심:
- sleep: 락 유지
- wait: 락 반납
TIMED_WAITING:
sleep(1000):
RUNNABLE → TIMED_WAITING
[1초 대기]
→ RUNNABLE (자동, 시간 경과)
wait(1000):
RUNNABLE → TIMED_WAITING (락 반납)
[1초 또는 notify]
→ RUNNABLE (락 재획득)
public class TimedWaitingExample {
// sleep — 폴링 간격
public void pollWithSleep() throws InterruptedException {
while (!isReady()) {
Thread.sleep(100); // TIMED_WAITING (100ms)
// 락 반납 X
}
}
// wait(ms) — 타임아웃 대기
public Shipment consumeWithTimeout(long timeoutMs) throws InterruptedException {
synchronized (lock) {
long deadline = System.currentTimeMillis() + timeoutMs;
while (queue.isEmpty()) {
long remaining = deadline - System.currentTimeMillis();
if (remaining <= 0) return null; // 타임아웃
lock.wait(remaining); // TIMED_WAITING (락 반납)
}
return queue.poll();
}
}
// join(ms) — 제한 대기
public void waitForWorkerWithTimeout(Thread worker) throws InterruptedException {
worker.join(5000); // TIMED_WAITING (최대 5초)
if (worker.isAlive()) {
log.warn("Worker still running after 5s");
}
}
}
TIMED_WAITING은?
답:
1. 정의:
진입:
vs WAITING:
sleep vs wait:
TERMINATED:
스레드의 run() 메서드가 종료된 상태.
스레드 생애의 끝.
진입:
- run() 정상 종료
- run() 예외로 종료
특징:
- 최종 상태
- 재시작 불가
- start() 다시 호출 시 예외
// 정상 종료
Thread t = new Thread(() -> {
doWork();
// run 종료 → TERMINATED
});
// 예외 종료
Thread t2 = new Thread(() -> {
throw new RuntimeException();
// 예외로 종료 → TERMINATED
});
Thread t = new Thread(() -> doWork());
t.start(); // RUNNABLE
t.join(); // TERMINATED 대기
// 재시작 시도
t.start(); // ❌ IllegalThreadStateException
// TERMINATED 스레드는 재시작 불가
전체 전이:
NEW
↓ start()
RUNNABLE ←──────────────────┐
↓ synchronized (락 없음) │ 락 획득
BLOCKED ────────────────────┤
↓ (RUNNABLE) │
RUNNABLE ←──────────────────┤
↓ wait()/join() │ notify/종료
WAITING ────────────────────┤
↓ (RUNNABLE) │
RUNNABLE ←──────────────────┤
↓ sleep()/wait(ms) │ 시간 경과/notify
TIMED_WAITING ──────────────┘
↓ run 종료
TERMINATED (최종)
| From | To | 트리거 |
|---|---|---|
| NEW | RUNNABLE | start() |
| RUNNABLE | BLOCKED | synchronized (락 없음) |
| RUNNABLE | WAITING | wait(), join() |
| RUNNABLE | TIMED_WAITING | sleep(ms), wait(ms) |
| BLOCKED | RUNNABLE | 락 획득 |
| WAITING | RUNNABLE | notify, 종료 |
| TIMED_WAITING | RUNNABLE | 시간 경과, notify |
| RUNNABLE | TERMINATED | run 종료 |
public class ThreadLifecycleComplete {
public void fullLifecycle() throws InterruptedException {
Thread worker = new Thread(() -> {
try {
// RUNNABLE
processData();
Thread.sleep(100); // TIMED_WAITING
synchronized (lock) { // BLOCKED 가능
lock.wait(1000); // TIMED_WAITING (락 반납)
}
// 작업 완료 → TERMINATED
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
log.info("Before start: {}", worker.getState()); // NEW
worker.start();
log.info("After start: {}", worker.getState()); // RUNNABLE
worker.join(); // TERMINATED 대기
log.info("After join: {}", worker.getState()); // TERMINATED
// worker.start(); // ❌ 재시작 불가
}
private final Object lock = new Object();
private void processData() { }
}
TERMINATED와 상태 전이는?
답:
1. TERMINATED:
종료:
재시작:
전이:
자바에는 RUNNING 상태가 없다:
Thread.State enum:
NEW, RUNNABLE, BLOCKED,
WAITING, TIMED_WAITING, TERMINATED
RUNNING 은 없음!
- 실행 중도 RUNNABLE
- 실행 가능도 RUNNABLE
RUNNING 이 없는 이유:
실제 CPU 실행 여부는 OS 스케줄러 소관.
- 자바는 OS 스케줄링 세부 모름
- 실행 중 ↔ 실행 가능 전이 너무 빠름
- 추적 의미 적음
→ RUNNABLE 로 통합
Thread t = new Thread(() -> {
while (true) {
// CPU 집약 작업 (실제 실행 중)
}
});
t.start();
System.out.println(t.getState()); // RUNNABLE (RUNNING 아님!)
// 실제 CPU 실행 중이어도 RUNNABLE
getState() 의 한계:
1. RUNNING 구분 X
- 실행 중인지 대기인지 모름
2. 스냅샷
- 호출 순간의 상태
- 즉시 변할 수 있음
3. I/O 대기 = RUNNABLE
- 실제 대기여도 RUNNABLE
→ 모니터링 시 참고용
→ 정확한 진단은 jstack, 프로파일러
# jstack — 스레드 덤프
$ jstack <pid>
# 출력 예:
"worker-1" #12 prio=5 ... RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
# RUNNABLE 이지만 socketRead = I/O 대기
"worker-2" #13 ... BLOCKED (on object monitor)
at com.ilic.Service.process(Service.java:50)
- waiting to lock <0x...> (a java.lang.Object)
# BLOCKED, 락 대기
"worker-3" #14 ... WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
# WAITING, wait 중
스레드 상태 모니터링:
1. jstack <pid>
- 스레드 덤프
- 상태 + 스택 트레이스
- 데드락 감지
2. VisualVM
- GUI
- 실시간 스레드 상태
- 시각화
3. JFR (Java Flight Recorder)
- 상세 프로파일링
- 스레드 활동
4. getState() (코드)
- 프로그래밍 방식
- 참고용
@Component
public class ThreadStateMonitor {
// getState() 활용 (참고용)
public Map<Thread.State, Long> getThreadStateDistribution(List<Thread> threads) {
return threads.stream()
.collect(Collectors.groupingBy(
Thread::getState,
Collectors.counting()));
// BLOCKED 가 많으면 락 경합 의심
}
// 데드락 감지 (ThreadMXBean)
public boolean detectDeadlock() {
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] deadlocked = bean.findDeadlockedThreads();
if (deadlocked != null) {
log.error("Deadlock detected! {} threads", deadlocked.length);
return true;
}
return false;
}
// 주의: getState() 는 RUNNING 반환 안 함
// I/O 대기도 RUNNABLE
// 정확한 진단은 jstack
}
getState()가 RUNNING을 반환하는가?
답:
1. 아니다:
이유:
한계:
진단:
| Q | 핵심 답변 |
|---|---|
| 6가지 상태? | NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED |
| NEW → RUNNABLE? | start() |
| BLOCKED? | synchronized 락 대기 |
| WAITING? | wait/join (무한) |
| TIMED_WAITING? | sleep/wait(ms) |
| BLOCKED vs WAITING? | 락 반납 vs notify/종료 |
| getState() RUNNING? | 아니다 (RUNNABLE) |
| RUNNABLE 두 의미? | 실행 중 + 실행 가능 |
| sleep vs wait? | 락 유지 vs 반납 |
| TERMINATED 재시작? | 불가 (예외) |
| I/O 대기 상태? | RUNNABLE (자바) |
답:
답:
답:
답:
답:
1. 6가지 상태
2. 핵심 차이
3. RUNNABLE과 getState
이번 Unit에서 스레드 상태를 봤다면, 다음은 Thread 클래스 상속으로 스레드 만들기.
🚀 Phase 3 — 스레드 만들고 다루기
✅ Unit 3.1 스레드 상태 다이어그램 ← 여기
⏭ Unit 3.2 Thread 클래스 상속
⏭ Unit 3.3 Runnable 인터페이스
⏭ Unit 3.4 데몬 스레드
⏭ Unit 3.5 join()
✅ Phase 1 — 동시성의 기초 (4 Unit)
✅ Phase 2 — 4분면 매트릭스 (3 Unit)
🚀 Phase 3 — 스레드 다루기 (1/5 진행)
총: 8/35 Unit