Thread 2
스레드 상태 관리 (Thread State & Control)
왜 쓰는지
스레드는 생성에서 종료까지 여러 상태를 거칩니다: - 언제 실행되는가 (RUNNABLE)? - 언제 멈추는가 (BLOCKED, WAITING)? - 다른 스레드를 어떻게 제어하는가?
상태를 이해하고 제어해야 예측 가능한 멀티스레드 프로그램을 만들 수 있습니다.
핵심: 스레드는 NEW → RUNNABLE → 실행/대기 → TERMINATED의 6가지 상태를 가지며, 상태 전이를 제어하는 메서드(join(), sleep(), wait(), notify())가 있습니다.
어떻게 쓰는지
스레드 상태 (Thread State)
public enum State {
NEW, // 생성됨, 아직 시작 안됨
RUNNABLE, // 실행 가능 상태 (CPU 할당 대기 또는 실행 중)
BLOCKED, // synchronized 락 대기
WAITING, // 다른 스레드의 신호 무기한 대기
TIMED_WAITING, // 다른 스레드의 신호 또는 타임아웃 대기
TERMINATED // 실행 완료
}
// 상태 확인
Thread thread = new Thread(() -> {
// 작업
});
System.out.println(thread.getState()); // NEW
thread.start();
System.out.println(thread.getState()); // RUNNABLE
스레드 상태 전이 다이어그램
NEW
↓
[start()]
↓
RUNNABLE (실행 대기 또는 실행 중)
├─→ [synchronized 진입] ──→ BLOCKED ──→ [락 획득] ──→ RUNNABLE
├─→ [sleep()] ──────────→ TIMED_WAITING ──→ [시간 경과] ──→ RUNNABLE
├─→ [wait()] ──────────→ WAITING ──→ [notify()] ──→ RUNNABLE
├─→ [join()] ──────────→ WAITING ──→ [대상 스레드 종료] ──→ RUNNABLE
└─→ [run() 완료] ─────→ TERMINATED
join(): 다른 스레드 완료 대기
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
try {
System.out.println("작업 시작");
Thread.sleep(2000); // 2초 대기
System.out.println("작업 완료");
} catch (InterruptedException e) {
System.out.println("작업 중단됨");
}
});
worker.start();
System.out.println("메인: worker 완료 대기");
worker.join(); // worker가 종료될 때까지 메인 스레드 대기
System.out.println("메인: 완료");
}
}
// 출력:
// 메인: worker 완료 대기
// 작업 시작
// 작업 완료
// 메인: 완료
sleep(): 스레드 일시 정지
// 1️⃣ 고정 시간 대기
try {
Thread.sleep(1000); // 1초 대기
} catch (InterruptedException e) {
System.out.println("수면 중단");
}
// 2️⃣ 진행률 표시
for (int i = 0; i <= 100; i += 20) {
System.out.println("진행 중: " + i + "%");
try {
Thread.sleep(500); // 500ms 대기
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 인터럽트 상태 복원
break;
}
}
// 3️⃣ 재시도 (Retry with delay)
int retries = 3;
for (int i = 0; i < retries; i++) {
try {
callApi();
break; // 성공
} catch (Exception e) {
if (i < retries - 1) {
Thread.sleep(1000 * (long) Math.pow(2, i)); // 지수 백오프
}
}
}
wait() / notify(): 스레드 간 신호
public class Buffer {
private Queue<String> queue = new LinkedList<>();
private int capacity = 5;
// 아이템 추가
public synchronized void put(String item) throws InterruptedException {
while (queue.size() >= capacity) {
wait(); // 버퍼 가득 차면 대기
}
queue.add(item);
System.out.println("추가: " + item);
notifyAll(); // 대기 중인 스레드 깨우기
}
// 아이템 제거
public synchronized String get() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // 버퍼 비면 대기
}
String item = queue.poll();
System.out.println("제거: " + item);
notifyAll(); // 대기 중인 스레드 깨우기
return item;
}
}
// 사용
Buffer buffer = new Buffer();
// Producer (추가하는 스레드)
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
buffer.put("아이템 " + i);
Thread.sleep(100);
} catch (InterruptedException e) {}
}
}).start();
// Consumer (제거하는 스레드)
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
buffer.get();
Thread.sleep(300);
} catch (InterruptedException e) {}
}
}).start();
yield(): CPU 양보
// CPU 시간을 다른 스레드에 양보
for (int i = 0; i < 10_000_000; i++) {
if (i % 100_000 == 0) {
Thread.yield(); // 다른 스레드가 실행될 기회 제공
}
}
언제 쓰는지
| 상황 | 메서드 | 이유 |
|---|---|---|
| 스레드 완료 대기 | join() |
순차 실행 필요 |
| 일시 정지 | sleep() |
작업 간격, 재시도 대기 |
| 조건 대기 | wait() |
조건 충족까지 대기 |
| 신호 전송 | notify() |
대기 중인 스레드 깨우기 |
| CPU 양보 | yield() |
다른 스레드 우선순위 |
장점
| 장점 | 설명 |
|---|---|
| 순차 제어 | join()으로 스레드 실행 순서 제어 |
| 효율적 대기 | sleep(), wait()로 불필요한 CPU 사용 방지 |
| 스레드 간 통신 | wait()/notify()로 동기화 |
| 상태 파악 | getState()로 상태 확인 |
단점
| 단점 | 설명 |
|---|---|
| 복잡성 | 6가지 상태와 메서드 관계 이해 필요 |
| 오버헤드 | 컨텍스트 스위칭, 동기화 비용 |
| 데드락 | 잘못된 wait()/notify() 사용 시 교착 가능 |
| 스핀락 | 지속적 상태 확인은 CPU 낭비 |
특징
1. join()의 활용
// 모든 워커 스레드가 완료될 때까지 대기
Thread[] workers = new Thread[10];
for (int i = 0; i < 10; i++) {
workers[i] = new Thread(() -> {
System.out.println("작업 중: " + Thread.currentThread().getName());
});
workers[i].start();
}
// 모든 워커 완료 대기
for (Thread worker : workers) {
worker.join();
}
System.out.println("모든 작업 완료");
2. 타임아웃이 있는 join()
Thread worker = new Thread(() -> {
try {
Thread.sleep(10000); // 10초
} catch (InterruptedException e) {}
});
worker.start();
// 3초만 대기
try {
worker.join(3000);
} catch (InterruptedException e) {}
if (worker.isAlive()) {
System.out.println("워커가 아직 실행 중");
}
3. InterruptedException 처리
Thread worker = new Thread(() -> {
try {
for (int i = 0; i < 100; i++) {
System.out.println("작업: " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("작업 중단됨");
// 인터럽트 상태 복원
Thread.currentThread().interrupt();
}
});
worker.start();
// 5초 후 중단
Thread.sleep(5000);
worker.interrupt(); // InterruptedException 발생
4. wait() vs sleep() vs yield()
| 메서드 | 목적 | 락 | 깨우기 |
|---|---|---|---|
sleep() |
시간 경과 대기 | 유지 | 타임아웃 |
wait() |
조건 대기 | 해제 | notify() |
yield() |
CPU 양보 | 유지 | 즉시 |
synchronized void example() {
Thread.sleep(1000); // 락 유지하면서 1초 대기
while (condition == false) {
wait(); // 락 해제하고 대기, notify 시 다시 획득
}
Thread.yield(); // 락 유지하면서 다른 스레드에 양보
}
주의할 점
❌ notify()와 notifyAll() 혼동
❌ wait()를 if로 사용
⚠️ sleep() 중 리소스 정리 미흡
💡 스레드 안전한 대기 패턴
정리
| 항목 | 설명 |
|---|---|
| NEW | 생성됨, 아직 미시작 |
| RUNNABLE | 실행 가능 |
| BLOCKED | 동기화 락 대기 |
| WAITING | 신호 무기한 대기 |
| TIMED_WAITING | 신호 또는 타임아웃 대기 |
| TERMINATED | 실행 완료 |
| join() | 스레드 완료 대기 |
| sleep() | 시간 대기 |
| wait()/notify() | 조건 동기화 |
관련 파일: - Thread 기본개념 — 멀티스레드의 이해 - synchronized — 동기화 메커니즘 - Thread1 — 스레드 생성 방법