티스토리 뷰
Java Thread는 Thread.State enum으로 정의된 6가지 상태를 가진다.
1. NEW
Thread 객체가 생성만 된 상태. 아직 스레드가 실행 중이 아님.
명시적으로 start()를 호출해야 스레드가 실행된다.
Thread t = new Thread(() -> {}); // NEW
스레드 실행
Thread t = new Thread(() -> {});
t.start();
2. RUNNABLE
start()가 호출되어 실행 가능한 상태. JVM 레벨에서는 실행 중이지만, OS 레벨에서는 CPU를 할당받아 실제 실행 중이거나 실행 대기 중일 수 있다. Java는 이 둘을 구분하지 않고 RUNNABLE로 통합해서 표현한다.
"실행 가능"이라는 표현을 쓰는 이유
OS 레벨에서는 Thread 상태가 더 세분화되어 있다.
- Running: 현재 CPU 코어에서 실제로 명령어를 실행 중
- Ready (Runnable): 실행할 준비는 끝났지만 CPU를 할당받지 못해 대기 큐에 있음
CPU 코어 수는 제한적인데 Thread는 수백, 수천 개일 수 있어 OS는 타임 슬라이스1 단위로 Thread들을 번갈아 CPU에 올렸다 내렸다 한다. 이 과정에서 Thread는 Running ↔ Ready 상태를 끊임없이 오간다.
JVM은 이 두 상태를 모두 RUNNABLE 하나로 통합한다.
3. BLOCKED
락을 획득하기 위해 대기하는 상태. synchronized 블록/메서드에 진입하려는데 다른 Thread가 이미 락을 잡고 있을 때의 상태이다.
synchronized(lock) { ... } // 락 대기 시 BLOCKED
여기서 "락"은 정확히는 모니터(Monitor) 락을 가리킨다. 그래서 BLOCKED가 뭔지 제대로 이해하려면 모니터 구조부터 짚고 가야 한다.
모니터(Monitor)란
모니터는 "한 번에 하나의 Thread만 접근할 수 있도록 보장하는 통제하는 장치이다." 락(Lock) + 조건 변수(Condition Variable) 를 하나로 묶어 객체에 내장한 형태로, Java는 이 모니터를 모든 객체에 내장하는 방식으로 구현했다. 이걸 intrinsic lock 또는 monitor lock이라고 부른다.
모니터 락의 구조
JVM 레벨에서 모니터는 세 부분으로 구성된다.
- Owner: 현재 락을 소유한 Thread
- Entry Set (진입 큐): 락을 획득하려고 대기 중인 Thread들 → BLOCKED 상태
- Wait Set (대기 큐):
wait()를 호출해서 조건을 기다리는 Thread들 → WAITING 상태
즉 BLOCKED는 Entry Set에 들어가 있는 상태고, Wait Set에 들어간 Thread는 WAITING으로 따로 구분된다.
ReentrantLock은 BLOCKED가 아니다
ReentrantLock은 모니터 락이 아니다. 별도의 Lock 구현체이고, 내부적으로 AQS(AbstractQueuedSynchronizer) + LockSupport.park()를 쓴다. 그래서 ReentrantLock.lock()으로 락을 기다리는 Thread는 BLOCKED가 아니라 WAITING 상태로 표시된다.
Thread dump 분석할 때 이 차이를 놓치면 락 경합의 원인을 엉뚱한 데서 찾게 된다.
4. WAITING
다른 Thread의 특정 작업이 완료될 때까지 무기한 대기하는 상태. 명시적으로 깨워야 RUNNABLE로 돌아온다.
5. TIMED_WAITING
지정된 시간 동안만 대기하는 상태. 시간이 지나면 자동으로 RUNNABLE로 전환된다.
6. TERMINATED
run() 메서드 실행이 완료되었거나 예외로 종료된 상태. 한 번 TERMINATED가 되면 다시 시작할 수 없다.
실무에서 자주 헷갈리는 포인트
BLOCKED vs WAITING
BLOCKED는 synchronized 락 경합에서만 발생하고, ReentrantLock.lock()에서 락을 기다릴 때는 내부적으로 LockSupport.park()를 쓰기 때문에 WAITING 상태가 된다. Thread dump를 분석할 때 이 차이를 모르면 락 경합을 놓치기 쉽다.
RUNNABLE인데 실제로는 I/O 대기
소켓 read, 파일 I/O 같은 블로킹 I/O를 수행 중인 Thread는 JVM에서 RUNNABLE로 표시된다. Thread dump에서 RUNNABLE이라고 해서 반드시 CPU를 쓰고 있는 건 아니라는 점이 TPS 병목 분석할 때 중요하다.
상태 전이는 단방향이 아님
RUNNABLE ↔ BLOCKED/WAITING/TIMED_WAITING 사이를 여러 번 오갈 수 있지만, NEW → RUNNABLE과 * → TERMINATED는 단방향이다.
- OS 스케줄러가 하나의 Thread에게 CPU를 점유하도록 허용하는 시간의 단위 [본문으로]
'언어 > Java' 카테고리의 다른 글
| 테스트 코드, 어디까지 검증해야 할까? (0) | 2026.02.15 |
|---|---|
| 테스트 더블은 코드에서 어떻게 표현되는가 (0) | 2026.02.14 |
| VO는 왜 불변이어야 하는가? (0) | 2026.02.08 |
| VO, 언제 쓰고 언제 쓰지 말아야 할까 (0) | 2026.02.08 |
| TDD 알아보기 (0) | 2026.02.06 |
