线程生命周期详解
——从 NEW 到 TERMINATED 的“一生”
一、为什么要吃透“生命周期”
- 写并发 Bug 的 ROOT CAUSE 90% 与状态有关:死锁、活锁、饥饿、CPU 飙高、内存泄漏……
- 面试必问:从“线程有哪些状态”到“WAITING 和 TIMED_WAITING 区别”再到“用 jstack 如何定位死锁”,答案都在生命周期里。
- 排查工具(jstack、Arthas、JProfiler)的输出直接打印状态,看不懂状态 = 看不懂报告。
二、五态模型总览(JDK 8+ 源码枚举)
java.lang.Thread.State
├─ NEW
├─ RUNNABLE
├─ BLOCKED
├─ WAITING
├─ TIMED_WAITING
└─ TERMINATED
(注:TIMED_WAITING 是 WAITING 的“限时版”,面试常合并讨论,下文单列为子状态。)
三、各状态深度拆解
-
NEW
源码:threadStatus=0,尚未调用 start()。
关键点:- 多次 start() 抛 IllegalThreadStateException。
- 此时 OS 线程还未创建,仅 Java 对象。
可视化:jstack 看不到,线程尚未进入 JVM 的线程调度列表。
-
RUNNABLE
源码:threadStatus=5,已进入 JVM,可能正在 CPU 上,也可能在就绪队列排队。
关键点:- 包含“Running”和“Ready”两个子状态,但 JVM 把它们合二为一,因此 Java 层面只有一个 RUNNABLE。
- 调用 yield() 会让出 CPU,但状态不变,仍是 RUNNABLE。
- I/O 阻塞(BIO)在 JVM 视角仍然是 RUNNABLE,因为底层 JVM 把阻塞交给了 OS,自己不会被挂起。
可视化:jstack 能看到“RUNNABLE”,堆栈顶多在 socketRead0、epollWait 等 native 方法。
-
BLOCKED
源码:threadStatus=3,等待进入 synchronized 块/方法,且尚未获得 monitor。
关键点:- 只与 synchronized 有关;LockSupport.park / ReentrantLock.lock() 不会进入 BLOCKED,而是 WAITING。
- 可形成“死锁”:线程 A BLOCKED 等待线程 B 持有的锁,B 又在等 A 的另一把锁。
可视化:jstack 会打印
"Thread-1" BLOCKED on <0x00000000d5c8b8a0> (a com.xxx.A)
owned by "Thread-0"
-
WAITING(无限期等待)
源码:threadStatus=6,等待“某个显式动作”唤醒。
触发 API:- Object.wait() / LockSupport.park()
- Thread.join()
- Lock.lockInterruptibly() 竞争锁失败进入 WAITING(AQS 内部)
关键点: - 必须等待其他线程“显式”唤醒(notify/notifyAll、unpark、lock 释放),否则永远 WAITING。
- 与 BLOCKED 区别:BLOCKED 等锁,WAITING 等“信号”。
可视化:jstack 堆栈顶常见
at java.lang.Object.wait(Native Method)
at java.lang.Thread.join(Thread.java:1260)
-
TIMED_WAITING(限时等待)
源码:threadStatus=7,带超时参数的 API 都会进入。
触发 API:- Thread.sleep(long)
- Object.wait(long)
- Thread.join(long)
- LockSupport.parkNanos / parkUntil
- ReentrantLock.tryLock(long, TimeUnit)
关键点: - 超时自动返回,无需其他线程唤醒。
- 使用频率最高的是 sleep,常被用作“心跳间隔”或“重试退避”。
可视化:jstack 堆栈顶
at java.lang.Thread.sleep(Native Method)
-
TERMINATED
源码:threadStatus=8,run() 执行完毕或抛出未捕获异常。
关键点:- 线程对象仍在堆中,可获取 ID、Name,但无法再 start。
- 若线程池中的任务抛异常,线程会被回收并新建替补,旧线程进入 TERMINATED。
可视化:jstack 不再列出;Thread.getState() 返回 TERMINATED。
四、状态跃迁大图(文字版)
NEW → start() → RUNNABLE
RUNNABLE ←→ CPU 调度(yield/time slice) → RUNNABLE
RUNNABLE → 竞争 synchronized 失败 → BLOCKED
BLOCKED → 获得 monitor → RUNNABLE
RUNNABLE → wait()/join()/park() → WAITING
WAITING → notify()/notifyAll()/unpark()/lock 释放 → RUNNABLE
RUNNABLE → sleep()/wait(long)/join(long)/parkNanos → TIMED_WAITING
TIMED_WAITING → 超时/notify/unpark → RUNNABLE
RUNNABLE → run() 结束 → TERMINATED
五、易混淆三连问(面试高频)
-
BLOCKED vs WAITING
等锁 vs 等信号;前者由 JVM 管,后者由程序员显式控制;jstack 输出关键字不同。 -
sleep 会释放锁吗?
不会。sleep 只让出 CPU,不释放已持有的 synchronized 锁;因此常配合 wait/notify 使用。 -
RUNNABLE 表示一定在 CPU 上跑吗?
否。Java 把 OS 的就绪队列和运行中合并,因此 RUNNABLE 也可能在排队。
六、实战:用 jstack 定位“卡死”
场景:Web 应用突然不响应,CPU 不高。
步骤:
- jps -l 找到 PID
- jstack -F PID > thread.dump
- 搜索“BLOCKED”或“WAITING”,发现大量线程
BLOCKED on <0x...> owned by "http-nio-8080-exec-3"
再搜 exec-3,发现它 WAITING on redis.clients.jedis...
结论:Redis 连接池耗尽导致同步锁竞争,形成链式阻塞。
解决:调大 maxTotal、加超时、改用 Lettuce 异步驱动。
七、一张图总结(保存即可)
start()
NEW --------------→ RUNNABLE ←---------┐
│ ↑ │
│ synchronized竞争失败 │ yield/time slice
│ ↓ │
│ BLOCKED │
│ ↑ │
│ │ 获得锁 │
│ └--------┐ │
│ │ │
│ wait/join/park │ sleep/wait(long)
│ │ │
↓ ↓ │
WAITING ←----notify/unpark---- TIMED_WAITING
│ │
│ │ 超时
│ │
└------------┬----------------┘
│
run()结束
↓
TERMINATED
八、结语
把“五态”刻进脑子,调试并发问题时你会像看彩色照片一样直观:
- 看到 BLOCKED 就找锁;
- 看到 WAITING 就找 notify/unpark;
- 看到 TIMED_WAITING 先问是不是 sleep 太久;
- 看到 RUNNABLE 但 CPU 高,就盯死循环或疯狂自旋。
吃透生命周期,才能真正“驾驭”线程,而不是被线程“玩弄”。祝你调试愉快,面试必过!