Java의 Thread.State enum에 따르면, 스레드는 다음과 같은 6가지 상태를 가질 수 있다.

그리고 문서를 자세히 읽어 보면

RUNNUBLE도 OS의 다른 리소스 때문에 "기다릴 수 있다."라고 말하고 있다.
❓ "RUNNABLE도 기다린다고? WAITING은 또 뭐고, BLOCKED는 뭐가 다르지?"
이게 궁금해서 WAITING / BLOCKED / RUNNABLE 에 대한 부분을 정리해봤다.
(참고로 java의 모든 인스턴스들은 모니터를 가지고 있다.)
synchronized (obj) {
obj.wait(); // WAITING 상태로 진입
}
Thread t1 = new Thread(() -> {
synchronized (lock) {
// 락을 오래 잡고 있음
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
// t1이 락을 가지고 있으면 여기서 BLOCKED
}
});
| 상태 | 기다림 이유 | 깨어나는 조건 |
|---|---|---|
| WAITING | 다른 스레드의 알림 | notify, join 종료 등 |
| BLOCKED | 락을 가진 스레드 | 락이 해제됨 |
| RUNNABLE | CPU 스케줄 대기 | OS 스케줄러 |
이 지점에서 놓치기 쉬운 아주 중요한 포인트 하나가 있다.
notifyAll()로 스레드를 깨운다고 해서 바로 RUNNABLE 상태가 되는 게 아니다!

번역: 깨어난 스레드는 현재 스레드가 이 객체의 잠금을 해제할 때까지 진행할 수 없습니다.
정확한 흐름은 다음과 같다.
1. A 스레드는 wait()을 호출해서 WAITING 상태에 진입
2. B 스레드는 notifyAll()을 호출해 A를 깨운다
3. A는 잠에서 깨어나지만, 락을 아직 못 얻었기 때문에 BLOCKED 상태로 전환
4. B가 synchronized 블록을 나가서 락을 반환하면
5. 그제서야 A는 락을 얻고 RUNNABLE 상태로 진입 후 실행
코드 예시로 확인해보자.
final Object lock = new Object();
Thread a = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("A: waiting...");
lock.wait(); // → WAITING 상태
System.out.println("A: resumed and running"); // 실행 전에 BLOCKED 상태 거침
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread b = new Thread(() -> {
synchronized (lock) {
System.out.println("B: notifying...");
lock.notifyAll(); // A를 깨움 → A는 BLOCKED 상태로 변경됨
try {
Thread.sleep(2000); // 락을 계속 보유
System.out.println("B: exiting synchronized block");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
이 예시에서 A는 notifyAll()로 깨어난 이후, 즉시 실행되지 않고, 락을 기다리며 BLOCKED 상태에 머물게 된다
WAITING, BLOCKED, RUNNABLE 세 가지 상태는 모두 "기다림"이라는 공통점이 있지만,
무엇을 기다리고 있는가? 에 따라 전혀 다른 의미와 동작을 갖고 있다.
또한, notify()나 notifyAll()을 통해 깨워도 바로 RUNNABLE로 가는 게 아니라, 락을 다시 얻을 때까지 BLOCKED 상태에 머무른다는 점은 공식 문서를 보지 않으면 간과하기 쉬운 디테일이었다.
언젠가 프로젝트를 하다가 Thread dump를 볼 일이 있으면 도움이 되지 않을까... 싶다.
https://docs.oracle.com/javase/8/docs/api/index.html?java/lang/Thread.State.html