JAVA · Vol.II · DAY 11 · 并发编程
线程生命周期:6 种状态与 12 次转换
从一道面试题开始
面试官双手交叉,不紧不慢地抛出这个问题:
候选人 A 脱口而出:"有 NEW、RUNNING、BLOCKED、WAITING、TERMINATED 五种"。面试官微微一笑——漏了一个,还多了一个(Java 里没有 RUNNING)。
候选人 B 胸有成竹:"六种!NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED"。面试官追问:那状态之间的转换分别由什么 API 触发?候选人沉默了。
再往深挖一层,这是生产排查的基本功:凌晨服务变慢,jstack 抓个线程转储,满屏都是 BLOCKED (on object monitor)——如果你不熟状态机,这屏输出对你就只是一堆英文。
这道题之所以高频,是因为它同时考察了三件事:
- 你是否真正读过
Thread.State的源码 - 你是否理解每种状态背后的操作系统调度原理
- 你能否把状态转换和具体 API 一一对应,并用 jstack 在生产里验证
发车之前,先问自己三个问题:
- RUNNABLE 到底包不包括"正在等待 CPU 时间片"的线程?
- BLOCKED 和 WAITING 的本质区别是什么?
Thread.sleep()和Object.wait()分别进入什么状态?释放锁吗?
带着这些问题,我们开始今天的线程之旅。
Thread.State 枚举:JDK 源码怎么说
很多人背过线程状态,但很少有人翻开 JDK 源码看一看。线程的所有状态定义在 java.lang.Thread.State 这个枚举中:
public enum State {
NEW, // Thread object created but start() not yet called.
RUNNABLE, // Executing in the JVM — may be waiting for OS resources (e.g. CPU).
BLOCKED, // Waiting to acquire a monitor lock (synchronized block/method).
WAITING, // Waiting indefinitely for another thread's action (wait, park, join).
TIMED_WAITING, // Waiting up to a specified time (sleep, wait(ms), parkNanos).
TERMINATED // The thread has completed execution.
}
注意两个容易被忽略的细节:
ReentrantLock.lock(),线程进入的是 WAITING 或 TIMED_WAITING(底层走 LockSupport.park),而不是 BLOCKED。完整状态机:6 个状态 × 12 次转换
下面这张图是线程生命周期的完整状态机。每一条箭头对应一次状态转换,箭头上标注了触发的 API。建议你放大仔细看——面试时能画出这张图,基本满分。
将 12 次转换整理成表格:
| # | 转换 | 触发 API |
|---|---|---|
| ① | NEW → RUNNABLE | Thread.start() |
| ② | RUNNABLE → BLOCKED | 进入 synchronized 块,锁被其他线程持有 |
| ③ | BLOCKED → RUNNABLE | 成功获取到监视器锁 |
| ④ | RUNNABLE → WAITING | Object.wait() / LockSupport.park() / Thread.join() |
| ⑤ | WAITING → BLOCKED | notify() / notifyAll(),唤醒后需重新竞争锁 |
| ⑥ | WAITING → RUNNABLE | LockSupport.unpark(),无需竞争锁直接进入就绪 |
| ⑦ | RUNNABLE → TIMED_WAITING | Thread.sleep(ms) / Object.wait(ms) / Thread.join(ms) / LockSupport.parkNanos() |
| ⑧ | TIMED_WAITING → RUNNABLE | 超时自动恢复 / 被 unpark() 唤醒 |
| ⑨ | TIMED_WAITING → BLOCKED | Object.wait(ms) 超时或唤醒后需重新竞争 synchronized 锁 |
| ⑩ | RUNNABLE → TERMINATED | run() 方法正常返回或抛出未捕获异常 |
| ⑪ | WAITING → TERMINATED | 被 Thread.interrupt() → 抛 InterruptedException 未被捕获 → 冲出 run() → 线程死亡 |
| ⑫ | TIMED_WAITING → TERMINATED | 同上(sleep / join(ms) / wait(ms) 中被打断) |
wait()/park() 抛出 InterruptedException,这个异常如果一路未捕获冲出 run(),线程才进入 TERMINATED。如果业务代码 catch 住了继续跑,线程还活着(只是中断标志被清掉了)。另外 BLOCKED 状态不能响应 interrupt——等 synchronized 锁的线程被打断只是记下标志,照样排队,拿到锁之后代码才有机会检查标志。六种状态逐一拆解 + 代码验证
| 状态 | 含义 | 进入方式(代码示例) | 退出条件 |
|---|---|---|---|
| NEW | 对象已创建,尚未调用 start() | Thread t = new Thread(); | 调用 t.start() |
| RUNNABLE | 正在运行 或 等待 CPU 调度 | t.start()、从阻塞返回 | 获取不到锁 / 主动等待 / 执行完毕 |
| BLOCKED | 等待获取 synchronized 监视器锁 | 进入 synchronized 块但锁被其他线程持有 | 获取到锁 → RUNNABLE |
| WAITING | 无限期等待另一个线程的动作 | Object.wait()、LockSupport.park()、Thread.join() | 被 notify / unpark → RUNNABLE 或 BLOCKED |
| TIMED_WAITING | 限时等待,到时间自动恢复 | Thread.sleep(ms)、Object.wait(ms)、LockSupport.parkNanos() | 超时 / 被唤醒 → RUNNABLE 或 BLOCKED |
| TERMINATED | run() 方法执行完毕 | run() 正常返回或抛出未捕获异常 | 终态,不可再转换 |
下面用代码验证每个状态:
// 1. NEW
Thread t1 = new Thread(() -> {});
System.out.println(t1.getState()); // NEW
// 2. RUNNABLE
Thread t2 = new Thread(() -> {
while (true) { Thread.yield(); }
});
t2.start();
System.out.println(t2.getState()); // RUNNABLE
// 3. BLOCKED
Object lock = new Object();
Thread holder = new Thread(() -> {
synchronized (lock) {
try { Thread.sleep(999999); } catch (Exception e) {}
}
});
holder.start();
Thread.sleep(100); // 等 holder 拿到锁
Thread t3 = new Thread(() -> {
synchronized (lock) {} // 拿不到锁 → BLOCKED
});
t3.start();
Thread.sleep(100);
System.out.println(t3.getState()); // BLOCKED
// 4. WAITING
Thread t4 = new Thread(() -> {
synchronized (lock) {
try { lock.wait(); } catch (Exception e) {}
}
});
// ... start + sleep → WAITING
// 5. TIMED_WAITING
Thread t5 = new Thread(() -> {
try { Thread.sleep(60000); } catch (Exception e) {}
});
t5.start();
Thread.sleep(100);
System.out.println(t5.getState()); // TIMED_WAITING
// 6. TERMINATED
Thread t6 = new Thread(() -> {});
t6.start();
t6.join();
System.out.println(t6.getState()); // TERMINATED
RUNNABLE 深挖:一个 JVM 状态,两个 OS 状态
第 2 站埋了个细节:RUNNABLE 包含"等待 CPU 时间片"。这句话值得单独展开,因为它揭示了 JVM 状态和 OS 状态不是一一对应的。
| JVM 状态 | 对应的 OS 线程状态 | 说明 |
|---|---|---|
| NEW | OS 线程还没创建 | start() 之前,只有 Java 对象,没有底层 pthread |
| RUNNABLE | Running(正在 CPU 上执行) | 占用一个 CPU 核,正在跑 run() |
| Ready(就绪,在运行队列里排队) | 时间片用完了 / 被更高优先级线程抢占了,但随时可被调度 | |
| BLOCKED | OS 层面阻塞(等 JVM 的 monitor,JVM 内部排队) | 注意:不是 OS 的 futex 阻塞,是 JVM 的 ObjectMonitor 管理 |
| WAITING / TIMED_WAITING | OS 层面阻塞(futex / 条件变量) | park 会真正让 OS 线程睡掉,不占 CPU |
| TERMINATED | OS 线程退出 | pthread 已 join 回收 |
三个推论,全是面试加分点:
- "jstack 里看到 RUNNABLE" ≠ 线程在跑。8 核机器上有 20 个 RUNNABLE 线程,同一时刻只有 8 个真的在 CPU 上,另外 12 个在 OS 运行队列里排队。CPU 密集型服务线程数远超核数时,大部分 RUNNABLE 线程其实都在排队——这是「线程越多吞吐越低」的底层原因。
- RUNNABLE 线程不会被「暂停」:时间片切换对 Java 代码完全透明,你的代码没有任何一行能观察到「我刚被抢占过」——所以 Java 层面不区分 READY/RUNNING,区分了也没用。
- yield() 只是「让一让」:把当前线程从 Running 挪回 Ready 队尾,不保证下次立刻再调度回来,别把它当同步手段用(第 4 站 demo 里用它只是制造一个可观测的 RUNNABLE 样本)。
NEW → RUNNABLE:start() 和 run() 的本质区别
状态机的第一条边(①)背后是新手最常踩的坑:直接调 run() 不会创建新线程。
Thread t = new Thread(() ->
System.out.println("线程名: " + Thread.currentThread().getName()));
// 写法 1:直接调 run() —— 普通方法调用,还是 main 线程在跑
t.run(); // 输出:线程名: main,状态永远是 NEW
// 写法 2:调 start() —— JVM 创建 OS 线程,在新线程里执行 run()
Thread t2 = new Thread(t::run);
t2.start(); // 输出:线程名: Thread-1(新线程!)
// 写法 3:对同一线程对象调两次 start()
t2.start(); // IllegalThreadStateException:线程是一次性的
为什么这样设计?start() 内部做的是重量级的事:创建底层 OS 线程(pthread)、分配栈内存、把线程状态从 NEW(threadStatus=0)翻成 RUNNABLE、再让 OS 调度器接管。这些工作只该发生一次:start() 开头就检查 threadStatus != 0 直接抛异常——这也是为什么线程不能「重启」,跑完的线程对象只能当尸体用,要再用只能 new 一个。
而 run() 只是个普通方法,谁调谁执行。调 run() 的经典错误后果:你以为并行跑了 100 个任务,其实是在一个线程里串行了 100 遍,总耗时变成 100 倍——而且不报任何错,静默变慢。正确姿势永远是用线程池的 execute/submit(见「线程池」篇),而不是手动 start。
RUNNABLE → BLOCKED:synchronized 的排队机制
一个 RUNNABLE 线程撞上被别人持有的 synchronized 锁,就进入 BLOCKED,在锁的 EntryList(ObjectMonitor 的阻塞列表)里排队。几个必须讲清楚的细节:
- 排队不等于 FIFO:synchronized 没有公平性保证,持锁线程释放后,JVM 从 EntryList 里挑一个唤醒(实现上不保证先排的先拿到),其他线程继续等。所以 synchronized 下的锁是「非公平」的——想公平,用
ReentrantLock(true)。 - BLOCKED 期间不响应 interrupt:中断只是记下标志位,线程照样排队,直到拿到锁进入临界区后,代码才可能检查标志(第 12 站细讲)。
- 持锁线程在临界区里 wait() 了会怎样:wait 会释放锁,排队的 BLOCKED 线程就能拿锁了——这就是为什么「持锁 + wait」是合法的,否则所有等这个锁的人全饿死。
jstack 里 BLOCKED 线程长这样(第 13 站展开):
"worker-1" #11 ...
waiting for monitor entry
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.OrderService.process(OrderService.java:42)
- waiting to lock <0x000000076bf62208> (a java.lang.Object)
waiting to lock <地址> 就是线索:拿这个地址去全文搜 - locked <同一地址>,就找到了持锁者——谁持有、持有者卡在哪一行,锁竞争的全貌就出来了。
RUNNABLE → WAITING:wait / join / park 三兄弟
三个 API,同一个状态(WAITING),但语义和用法差别很大:
| API | 等什么 | 谁唤醒 | 前提条件 | 典型用途 |
|---|---|---|---|---|
obj.wait() | 等另一个线程调 notify/notifyAll | notify 挑一个,notifyAll 全唤醒 | 必须持有 obj 的锁,否则 IllegalMonitorStateException | 生产者-消费者、条件等待 |
t.join() | 等线程 t 结束 | t 的 run() 结束(内部等价于对 t.wait()) | 无(任何线程可调用) | 主线程等子任务完成 |
LockSupport.park() | 等 unpark 信号(许可) | LockSupport.unpark(t) | 无(不需要持锁) | AQS 底层、线程池工作线程取任务 |
三个易错点:
- wait() 是实例方法,sleep() 是静态方法:
Object.wait()操作的是「这个对象的监视器」,Thread.sleep()只是让当前线程睡——所以 wait 必须在 synchronized 块里(要操作 monitor),sleep 在哪都能调。 - park 是「许可制」:unpark 可以在 park 之前调用(许可先存着,后面 park 直接通过)——这比 wait/notify「先 wait 后 notify 才能匹配」灵活得多,AQS 的队列唤醒全靠它。
- wait() 会释放锁,sleep() 不会:这是「持锁线程想让路」时唯一正确的睡法——sleep 着持锁,所有竞争者全 BLOCKED,直接堵死。
RUNNABLE → TIMED_WAITING:四个限时 API 的取舍
| API | 释放锁? | 响应中断? | 超时行为 | 适合场景 |
|---|---|---|---|---|
Thread.sleep(ms) | 不释放 | 是(抛 InterruptedException) | 到点自动恢复 | 简单延时、轮询间隔 |
obj.wait(ms) | 释放 | 是 | 到点自动恢复,重新竞争锁 | 持锁线程限时等条件 |
t.join(ms) | 不涉及锁 | 是 | 超时后不再等 t | 限时等任务完成 |
LockSupport.parkNanos(ns) | 不涉及锁 | 是(但不清许可) | 到点或 unpark 恢复 | 框架底层(AQS、线程池) |
// ❌ 反例:持锁 sleep —— 10 秒内所有竞争线程全部 BLOCKED
synchronized (lock) {
Thread.sleep(10_000); // 锁一直攥在手里!
doWork();
}
// ✅ 正解 1:真的需要等条件 → wait(释放锁)
synchronized (lock) {
while (!condition) lock.wait(10_000); // 等的时候锁放出去了
doWork();
}
// ✅ 正解 2:只是任务本身慢 → 别在锁里干慢事
synchronized (lock) {
submitHeavyTask(); // 锁内只做登记,重活丢给线程池
}
唤醒路径:WAITING 醒来 ≠ 立刻执行
WAITING / TIMED_WAITING 的线程有三条醒来的路:被通知(notify / unpark)、超时、被中断(interrupt → InterruptedException)。但「醒来」之后发生什么,是这里最容易被误解的地方:
① 被唤醒 ≠ 继续执行:wait()/wait(ms) 醒来后要先重新竞争 monitor 锁(拿不到就进 BLOCKED,对应状态机 ⑤⑨ 边)
② park() 醒来没有锁的问题,但只是「有资格跑」,还要 OS 调度
③ 条件必须重新检查——醒来不代表条件成立
第③点直接决定了生产代码里一个铁律:wait 必须包在 while 里,不能用 if:
synchronized (lock) {
// ❌ if:醒来直接往下走,但条件可能早被别人消费掉了
if (deque.isEmpty()) notEmpty.await();
T v = deque.removeFirst(); // 可能 remove 一个空队列 → 出错的不是报错,是数据错乱
// ✅ while:醒来后重新判断,不满足继续等
while (deque.isEmpty()) notEmpty.await();
T v = deque.removeFirst(); // 此刻队列一定非空
}
必须 while 的两个原因,面试都要说出来:
- 虚假唤醒(spurious wakeup):JVM 规范允许 wait() 在没有 notify 的情况下返回(底层 OS/硬件层面的设计),这是语言层面兜底的,业务代码必须自己扛。
- notify 之后条件可能被抢走:notifyAll 唤醒 5 个消费者,锁释放后 5 个人逐个重新竞争锁,第一个人拿走唯一元素,后四个醒来时条件已经不成立了——if 版代码就会在空队列上翻车。
顺带把 notify() vs notifyAll() 定个调:能确定「只有一个等待者需要这个条件」时 notify 省开销;条件语义是「全体相关方都可能关心」时(如队列容量既约束生产者又约束消费者)必须 notifyAll,漏唤醒比多唤醒致命得多。
wait/notify vs sleep:一张表终结混淆
第 8、9 站分别讲了 wait 家族和 sleep 家族,这一站把它们放在同一张表里对撞——这是面试被追问频率第二高的问题:
| 维度 | Object.wait()/notify() | Thread.sleep() |
|---|---|---|
| 所属 | Object 的实例方法(操作 monitor) | Thread 的静态方法(操作当前线程) |
| 释放锁? | 释放(等的时候把 monitor 让出来) | 不释放(攥着锁睡) |
| 调用前提 | 必须在 synchronized 块内,否则 IllegalMonitorStateException | 任何位置都可以 |
| 恢复方式 | notify/notifyAll / 超时 / 中断 | 超时(到点必醒)/ 中断 |
| 能否指定时长 | 能(wait(ms) / wait(ms, ns)) | 必须(sleep 就是要睡一会儿) |
| 响应中断 | 是,抛 InterruptedException 且清除中断标志 | 是,抛 InterruptedException 且清除中断标志 |
| 本质 | 线程间的协作(一个喊,一个应) | 线程的自我延时(不需要别人配合) |
一句话选型:需要「等某个条件成立」→ wait 家族;只是「过一会儿再来」→ sleep。生产里最常见的错误是拿 sleep 当 wait 用(轮询代替协作,延迟高还浪费 CPU),以及拿 wait 当 sleep 用(忘了 notify,线程永久 WAITING)——两种错误 jstack 里都看得见,一个是 TIMED_WAITING (sleeping),一个是 WAITING (on object monitor) 停了好几天。
interrupt 深挖:它不是「打断」,是「发信号」
Thread.interrupt(t) 对处于不同状态的线程,行为完全不同——这是「中断」机制最常被答歪的地方:
| 目标线程状态 | interrupt 的效果 |
|---|---|
| RUNNABLE(正常跑着) | 只设置中断标志位(boolean),线程继续跑,什么都不发生——要它自己检查 |
| WAITING(wait/park/join 中) | 抛出 InterruptedException,并清除中断标志 |
| TIMED_WAITING(sleep/join(ms)/wait(ms) 中) | 同上:抛异常 + 清标志 |
| BLOCKED(等 synchronized) | 不响应:只记标志,照样排队;拿到锁后代码才有机会看到标志 |
| TERMINATED | 无效 |
两个 API 读标志,语义不一样,别混:
Thread.currentThread().isInterrupted(); // 只读不清:看标志还在不在
Thread.interrupted(); // 读且清:一次性消费(静态方法,只能查当前线程)
// 长循环线程的优雅退出惯例:定期检查标志
while (!Thread.isInterrupted()) {
processOneTask(); // 单步之间检查,退出延迟 = 单步耗时
}
为什么中断状态要「抛异常 + 清标志」?因为异常已经把信号送达了(你的 catch 里处理),再留着标志会引发二次误伤——下一轮循环 Thread.isInterrupted() 又 true,线程莫名其妙退出。惯例:在 catch 里如果决定继续跑,可以 Thread.currentThread().interrupt() 把标志重新补上,把决定权交还给上层;如果决定退出,就什么都不做让线程自然结束。
isInterrupted(),可中断点(IO、park、sleep)让异常正常抛出。这样 thread.interrupt() 才是真正的停止按钮,否则它只是一根没人接的电话线。TERMINATED:正常退场与异常猝死
线程进 TERMINATED 只有两条路:run() 正常返回,或 run() 里抛出未捕获的异常。第二条路比表面看起来更危险:
- 未捕获异常 = 线程猝死:异常冲出 run() 后,JVM 不会杀死整个进程,只是让这一个线程死亡,并把异常交给
UncaughtExceptionHandler(默认打印到 stderr)。业务后果:你以为是常驻的工作线程,其实早就死了,任务积压却毫无告警——「线程池里线程越来越少」的排查第一步就是看 UncaughtExceptionHandler 日志。 - 线程对象不可复活:TERMINATED 后 start() 直接抛
IllegalThreadStateException。需要「再来一次」就 new 新线程(或更好的:线程池复用)。 - 怎么知道它跑了没有 / 跑得对不对:
t.join()等它结束(自己进 TIMED_WAITING/WAITING);t.isAlive()查是否还在跑;有返回值的任务用FutureTask/线程池submit,future.get()能拿到结果或异常(呼应「线程创建」篇的 execute vs submit 异常处理)。
ThreadFactory factory = r -> {
Thread t = new Thread(r, "worker-" + seq.getAndIncrement());
t.setUncaughtExceptionHandler((th, ex) -> {
log.error("工作线程 {} 意外死亡", th.getName(), ex);
metrics.inc("thread.death"); // 告警:常驻线程死亡 = 事故
});
return t;
};
jstack 实战:从线程转储中读状态
面试官的终极杀招:"给你一个线程转储,你能分析出什么问题?"掌握 jstack 输出格式,是你从"背八股"进阶到"真会用"的标志。
先看一段真实的 jstack 输出(节选):
$ jstack 12345
Full thread dump OpenJDK 64-Bit Server VM (17.0.2+8):
"worker-1" #11 prio=5 os_prio=0 tid=0x00007f3a1c0e8800 nid=0x2a03
waiting for monitor entry [0x00007f3a08ffe000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.OrderService.process(OrderService.java:42)
- waiting to lock <0x000000076bf62208> (a java.lang.Object)
at com.example.Worker.run(Worker.java:18)
"worker-2" #12 prio=5 os_prio=0 tid=0x00007f3a1c0ea000 nid=0x2a04
in Object.wait() [0x00007f3a08efe000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076bf62208> (a java.lang.Object)
at java.lang.Object.wait(Object.java:328)
at com.example.Consumer.consume(Consumer.java:35)
"timer-1" #13 prio=5 os_prio=0 tid=0x00007f3a1c0ec000 nid=0x2a05
sleeping [0x00007f3a08dfe000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at com.example.Scheduler.poll(Scheduler.java:22)
"main" #1 prio=5 os_prio=0 tid=0x00007f3a1c010800 nid=0x2901
runnable [0x00007f3a23ffe000]
java.lang.Thread.State: RUNNABLE
at com.example.App.main(App.java:15)
逐行解读的关键字段:
| 字段 | 含义 | 示例 |
|---|---|---|
nid | 操作系统原生线程 ID(十六进制) | 0x2a03 → 用 top -Hp 查 CPU 占用 |
Thread.State | Java 层面的线程状态 | BLOCKED / WAITING / RUNNABLE ... |
waiting for monitor entry | 正在排队等 synchronized 锁 | 对应 BLOCKED |
in Object.wait() | 调用了 wait() 正在等待信号 | 对应 WAITING |
waiting to lock <addr> | 等待获取的锁对象地址 | 和"locked <addr>"配对分析 |
- locked <addr> | 当前持有的锁 | 找到谁持有了这把锁 |
先读「状态分布」,再读「具体线程」——这是老手和新手的第一区别:
- 大量 WAITING 通常正常:线程池的空闲工作线程 park 在取任务、消费者 wait 在空队列、RPC 线程等 IO——WAITING 是「礼貌地等」,本身不是病。
- 大量 BLOCKED = 锁竞争热点:几十个线程 waiting to lock 同一个地址,而持锁线程栈顶还卡在某行慢代码(比如锁内调 RPC)——这就是要修的问题:锁粒度太大或锁内干了重活。
- RUNNABLE 但 CPU 打满:找 nid 对应
top -Hp的高耗线程,看它是不是在忙等循环(没 sleep 的 while)或 GC(VM 线程)。 - WAITING 停在同一条 wait() 好几天:被唤醒的 notify 漏发了,或者条件永远不成立——配合 while 条件排查。
确认「某个线程到底卡在哪」的排查三板斧:
# 第一步:导出线程转储(-l 带锁信息)
jstack -l <pid> > thread_dump.txt
# 第二步:统计状态分布,先看大局
grep -o "Thread.State: [A-Z_]*" thread_dump.txt | sort | uniq -c
# 第三步:有 BLOCKED 的话,按锁地址找持有者
grep -A 5 "BLOCKED" thread_dump.txt
grep "0x000000076bf62208" thread_dump.txt
# → 找到 "locked <0x...>" 的线程 = 持锁者,看它的栈顶在干什么
# 如果 jstack 末尾出现 "Found one Java-level deadlock":
# → 确认死锁,按输出的循环依赖链修代码(通常是加锁顺序不一致)
生产症状 → 状态 → 病根:速查表
把 14 站的排查经验压成一张「症状速查表」,下次 oncall 直接对号入座:
| jstack 症状 | 常见病根 | 处置方向 |
|---|---|---|
几十个线程 BLOCKED (on object monitor),waiting to lock 同一地址 | 锁竞争热点:锁内慢代码(RPC/IO/大循环)或锁粒度太大 | 让持锁者栈顶说话:缩小临界区、锁外做慢事、或换无锁/分段方案 |
线程池线程全 WAITING 在 ArrayBlockingQueue.take() | 池空闲(正常)或上游断流(异常) | 对照 QPS 曲线:正常流量=健康;流量还在但没人取任务=提交端故障 |
业务线程 WAITING (on object monitor) 停在自定义 wait() | notify 漏发 / 条件永不成立 / 虚假唤醒没 while 兜底 | 检查 notify 路径是否所有分支都覆盖;wait 改 while |
线程 WAITING 在 SocketRead / read | 下游 RPC / DB 慢(线程在等 IO 返回) | 查下游 RT 和连接池;给调用加超时(无超时的阻塞调用是定时炸弹) |
大量线程 RUNNABLE,CPU 接近 100% | 忙等循环 / 死循环 / GC 线程 / 正则回溯(ReDoS) | top -Hp 定位 nid → jstack 看栈顶;正则类问题给输入加白名单/长度限制 |
| 线程数持续上涨,旧线程不消失 | 线程泄漏:new Thread 没进池 / 池没 shutdown | 看线程名归类;所有线程收编进命名线程池 |
| 常驻工作线程数量只减不增 | 线程猝死:run() 里未捕获异常 | 查 UncaughtExceptionHandler 日志;任务体包 try-catch |
jstack 末尾 Found one Java-level deadlock | 多把锁加锁顺序不一致(A→B vs B→A) | 统一全局加锁顺序;或缩小到单锁;复杂场景用锁分层 |
这一篇你掌握了什么
把今天的核心知识压缩成一张速查表:
| 状态 | 一句话 | 关键 API(进入) | 关键 API(退出) |
|---|---|---|---|
| NEW | 创建了但没 start | new Thread() | start() |
| RUNNABLE | 正在跑或排队等 CPU(含 OS Ready) | start() / 从其他状态恢复 | 阻塞 / 等待 / 结束 |
| BLOCKED | 等 synchronized 锁(不响应 interrupt) | 进入 sync 块但锁被占用 | 拿到锁 / 持锁者释放 |
| WAITING | 等别人通知(wait/park/join) | wait() / park() / join() | notify / unpark / 中断 |
| TIMED_WAITING | 等一会儿,到点自动醒 | sleep(ms) / wait(ms) / join(ms) | 超时 / 被唤醒 / 中断 |
| TERMINATED | 跑完了(或异常猝死),不可逆 | run() 结束 | 无 |
高频面试追问 & 标准答案:
A: 六个维度——sleep 是 Thread 静态方法、wait 是 Object 实例方法;sleep 不释放锁、wait 释放锁;wait 必须在 synchronized 内调用否则 IllegalMonitorStateException、sleep 随处可调;wait 靠 notify/notifyAll/超时/中断恢复、sleep 靠超时/中断;wait 是线程协作(等条件)、sleep 是自我延时;底层 wait 进 monitor 的 WaitSet、sleep 由 JVM 调度挂起。
A: BLOCKED 是被动等 synchronized 锁,JVM 自动排队,不响应中断;WAITING 是主动调 wait/park/join 等信号,可被 notify/unpark/interrupt 唤醒。关键推论:ReentrantLock.lock() 拿不到锁产生的是 WAITING(底层 park),不是 BLOCKED——所以 jstack 里一片 WAITING 不一定是问题,一片 BLOCKED 才是锁竞争热点。
A: wait() 立即抛出 InterruptedException,同时清除中断标志(异常已送达,标志不必再留)。线程从 WAITING 恢复,要重新竞争锁(拿不到就 BLOCKED)。如果业务代码不 catch 让它冲出 run(),线程就进 TERMINATED——这就是状态机里 WAITING → TERMINATED 那条边。
A: 调用 Thread.interrupt()。如果目标线程处于 sleep / wait / join(TIMED_WAITING 或 WAITING),会立即抛出 InterruptedException 并清除中断标志。如果目标线程正在 RUNNABLE 状态运行,只设置中断标志位,需要目标线程自己检查 Thread.interrupted() 并退出。所以「可中断」是线程作者的契约:长循环要检查标志,可中断点要让异常正常传播。
核心知识点回顾
- Thread.State 有且仅有 6 个值:NEW / RUNNABLE / BLOCKED / WAITING / TIMED_WAITING / TERMINATED。
- 状态之间有 12 次转换,每次都由明确的 API 触发(能徒手画出状态机图)。
- RUNNABLE = OS 的 Running + Ready;jstack 里的 RUNNABLE 不等于在跑。
start()创建 OS 线程(一次性),run()是普通方法调用。- BLOCKED 专等 synchronized 锁、非公平、不响应中断;ReentrantLock 等锁是 WAITING。
- wait 三兄弟:wait(放锁+协作)、park(许可制)、join(等线程);sleep 不放锁。
- 唤醒 ≠ 执行:wait 醒来要重竞争锁,条件必须 while 重新检查(虚假唤醒 + 抢跑)。
- interrupt 是发信号不是打断:RUNNABLE 只设标志,WAITING 抛异常清标志,BLOCKED 不响应。
- 未捕获异常 = 线程猝死:给常驻线程装 UncaughtExceptionHandler + 打点告警。
- jstack:先读状态分布再读单线程;大量 WAITING 通常正常,大量 BLOCKED = 锁热点。
一句话总结
线程状态机是「API 的投影」:每个状态都是某个调用(start/wait/park/sleep/同步失败)的指纹,jstack 就是拿着指纹库读现场——状态分布定性质,锁地址和栈顶定嫌疑人。
🎤 面试 30 秒总结
"Java 线程有 6 个状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。RUNNABLE 包含 OS 层的 Running 和 Ready——JVM 不区分,因为 Java 代码观测不到调度。12 次转换都由明确 API 触发:start 进 RUNNABLE;synchronized 抢锁失败进 BLOCKED(不响应中断);wait/park/join 进 WAITING,sleep/wait(ms)/join(ms) 进 TIMED_WAITING。唤醒后 wait 家族要重新竞争锁,所以条件必须 while 检查,防虚假唤醒和抢跑。BLOCKED 和 WAITING 的本质区别是「被动等锁 vs 主动等信号」,ReentrantLock 等锁走 park 所以是 WAITING 不是 BLOCKED。生产上 jstack 先看状态分布:大量 WAITING 通常正常,大量 BLOCKED 找同一锁地址的持锁者审栈顶;线程死亡要看 UncaughtExceptionHandler。"
Comments · 评论