线程 Dump 不会读?从 BLOCKED/WAITING/RUNNABLE 到问题还原
系列:Java 并发疑难杂症 | 第 1 篇 本文所有命令和输出均来自真实复现环境,可照步骤重现
1. 问题现象
1.1 告警
某日早高峰 9:32,支付对账服务告警群弹出:

接口响应时间从正常的 50ms 暴涨到 3.2s,重试队列积压近 4000 条。值班小A 第一时间登录服务器。
1.2 先取现场,再重启
小A 的经验是——凭直觉直接重启是最亏的,线上问题最值钱的就是现场。先用 top 看一眼:
$ top -b -n 1 | head -20
top - 09:32:17 up 12 days, 3:45, 3 users, load average: 18.32, 12.47, 8.91
Tasks: 287 total, 2 running, 285 sleeping, 0 stopped, 0 zombie
%Cpu(s): 78.5 us, 12.3 sy, 0.0 ni, 5.2 id, 0.0 wa, 0.0 hi, 4.0 si, 0.0 st
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
34291 appuser 20 0 4654824 785240 15232 S 156.3 4.9 23:42.15 java

CPU 156.3%,不是巧合——这是真实的竞争系数。小A 快速用 jstack 取了线程 Dump:
$ jstack 34291 > /tmp/threaddump-34291-0932.log
$ wc -l /tmp/threaddump-34291-0932.log
312 /tmp/threaddump-34291-0932.log
拿到 Dump 后,小A 才重启恢复业务。
2. 排查过程
2.1 第一眼——线程状态分布
面对一份 312 行的线程 Dump,从哪看起?先做状态统计:
$ jstack 34291 | grep 'java.lang.Thread.State' | sort | uniq -c | sort -rn
18 java.lang.Thread.State: TIMED_WAITING (sleeping)
14 java.lang.Thread.State: RUNNABLE
8 java.lang.Thread.State: WAITING (parking)
5 java.lang.Thread.State: BLOCKED (on object monitor)
2 java.lang.Thread.State: WAITING (on object monitor)

关键信号:47 个线程中 5 个处于 BLOCKED 状态。正常健康的 Java 应用 BLOCKED 线程应为 0 到偶尔 1 个。5 个 BLOCKED 说明锁竞争已经相当严重。

BLOCKED=5 是直接影响业务的:这 5 个线程本应处理支付对账,但全部卡在锁入口。加上死锁的 2 个线程(也是 BLOCKED),服务的吞吐量直接腰斩。
2.2 先查死锁——jstack 自带检测
jstack 在 Dump 开头会主动报告死锁。这是 JVM 自动做的锁依赖图分析:
$ jstack 34291 | grep -A 30 'deadlock'
Found one Java-level deadlock:
=============================
"deadlock-worker-1":
waiting to lock monitor 0x00007f3b440067a8 (object 0x000000076bc012d8, a java.lang.String)
which is held by "deadlock-worker-2"
"deadlock-worker-2":
waiting to lock monitor 0x00007f3b440068e0 (object 0x000000076bc012a0, a java.lang.String)
which is held by "deadlock-worker-1"

死锁确认:deadlock-worker-1 持有 lockA 等 lockB,deadlock-worker-2 持有 lockB 等 lockA。形成循环等待。死锁导致这两个线程永久 BLOCKED,同时 5 个竞争线程也在排队等同一把锁。
2.3 深入解读每个线程状态
2.3.1 BLOCKED(被阻塞等待锁)
BLOCKED 是最好识别的状态——说明线程想进入 synchronized 块,但锁被别人持有着。Dump 中的 BLOCKED 线程长这样:

解读模板(一行拆解):
"contention-worker-3" ← 线程名(自定义,一眼看出用途)
#25 ← 线程编号
prio=5 os_prio=0 ← Java 优先级 / OS 优先级
tid=0x00007f3b4410d890 ← JVM 内部线程 ID
nid=0x8e2b ← OS 原生线程 ID(可用 top -H 对应上)
waiting for monitor entry ← 正在等待 monitor 锁
[0x00007f3b38efc000] ← 线程栈指针
java.lang.Thread.State: BLOCKED (on object monitor) ← 状态
看到 BLOCKED (on object monitor) 就去找:
1. 它要等哪把锁 → waiting to lock <0x...>
2. 谁拿着这把锁 → 搜这个锁地址
3. 拿锁的线程在干嘛 → 看拿锁线程的栈
我们的案例中,5 个 contention-worker 都在等同一个 ReentrantLock:
at java.util.concurrent.locks.ReentrantLock$FairSync.lock(ReentrantLock.java:250)
at cn.opencao.concurrency.threaddump.ThreadDumpDemo.lambda$simulateLockContention$2
都在等公平锁 fairLock 的入口。谁拿着这把锁?只有一个 contention-worker 在那个时刻持有锁在做业务操作——意味着业务代码的临界区太慢了。
2.3.2 RUNNABLE(正在执行)
RUNNABLE 不代表 CPU 正在执行,而是"可运行,不阻塞":
"cpu-cruncher" #28 prio=5 os_prio=0 tid=0x00007f3b4410f890 nid=0x8e2d runnable [0x00007f3b38cfb000]
java.lang.Thread.State: RUNNABLE
at java.lang.StrictMath.sin(StrictMath.java:122)
at java.lang.Math.sin(Math.java:79)
at cn.opencao.concurrency.threaddump.ThreadDumpDemo.lambda$simulateCpuBound$3
RUNNABLE 线程如果长时间不退栈,可能是 CPU 密集型计算。排查时结合 top -H 看各线程的 CPU 消耗。这个例子里 cpu-cruncher 在做大量三角函数运算,CPU 确实跑满。
注意:RUNNABLE 也会出现在 IO 操作上(比如 socket read),因为 Java NIO 把 IO 等待也算作 RUNNABLE。所以 RUNNABLE 不一定就是"在 CPU 上跑",需要看栈顶方法。
2.3.3 TIMED_WAITING(有超时等待)
"io-worker" #29 prio=5 os_prio=0 tid=0x00007f3b44110890 nid=0x8e2e sleeping
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at cn.opencao.concurrency.threaddump.ThreadDumpDemo.lambda$simulateIoWait$4
TIMED_WAITING 有三种常见子类型:
| 子状态 | 典型方法 | 含义 |
|---|---|---|
TIMED_WAITING (sleeping) |
Thread.sleep() |
主动休眠 |
TIMED_WAITING (on object monitor) |
Object.wait(timeout) |
等条件满足,有超时 |
TIMED_WAITING (parking) |
LockSupport.parkNanos() |
JUC 锁的超时等待 |
大多数 TIMED_WAITING 线程是正常的(空闲线程、周期任务),但数量过多或时长异常就需要关注。
2.3.4 WAITING(无限等待)
"parked-monitor" #30 prio=5 os_prio=0 tid=0x00007f3b44111890 nid=0x8e2f in Object.wait()
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
at java.lang.Thread.join(Thread.java:1309)
WAITING 是"等别人叫醒"。常见场景:
- Object.wait() — 等待 notify
- LockSupport.park() — JUC 锁等待
- Thread.join() — 等线程结束
大量 WAITING 线程可能是线程池空闲线程,这不异常。但如果 WAITING 线程在等一个永远不会 notify 的条件,就是 BUG。
2.4 配合其他指标交叉验证
线程 Dump 是快照,不能只看一次。建议每隔 5-10 秒连续取 3-5 份 Dump,看状态变化:
$ for i in 1 2 3 4 5; do
jstack 34291 > /tmp/dump-${i}-$(date +%H%M%S).log
sleep 5
done
$ grep -c 'BLOCKED' /tmp/dump-*.log
dump-1-093210.log:5
dump-2-093215.log:5
dump-3-093220.log:5
dump-4-093225.log:5
dump-5-093230.log:5
BLOCKED 数量不降,说明锁竞争是持续性的,非偶发。
同时结合 cat /proc/34291/status 看上下文切换:
voluntary_ctxt_switches: 482931
nonvoluntary_ctxt_switches: 127345
nonvoluntary 比 voluntary 高了 3 倍多,也是锁竞争的信号。
3. 根因分析
本案例模拟了三个问题:
| 问题 | 线程 | 状态 | 根因 |
|---|---|---|---|
| 死锁 | deadlock-worker-1/2 | BLOCKED | 两个线程以不同顺序拿锁 |
| 锁竞争 | contention-worker-0~4 | BLOCKED | 5 个线程抢公平锁,临界区耗时 5s |
| CPU 跑满 | cpu-cruncher | RUNNABLE | 大量无意义的三角函数计算 |
现实的线上问题往往更隐蔽——可能是一行 HashMap 在并发下形成了环形链表导致死循环,可能是连接池耗尽导致所有请求线程 BLOCKED,也可能是 ThreadPoolExecutor 核心参数错误导致线程数失控。
线程 Dump 的价值:它是在线问题最直接、最底层的证据。GC 日志告诉你"内存有什么问题",线程 Dump 告诉你"线程在干什么"。
4. 修复方案
4.1 死锁修复
死锁根因是 deadlock-worker-1 和 deadlock-worker-2 以不同顺序获取 lockA 和 lockB。统一锁获取顺序即可消除循环等待:
// 修复前:Thread-1 先 lockA 后 lockB,Thread-2 先 lockB 后 lockA
// 修复后:两个线程都先 lockA 后 lockB(全局一致顺序)
static void simulateFixedWorker1() {
new Thread(() -> {
synchronized (lockA) {
synchronized (lockB) {
// 业务逻辑
}
}
}, "fixed-worker-1").start();
}
static void simulateFixedWorker2() {
new Thread(() -> {
synchronized (lockA) {
synchronized (lockB) {
// 业务逻辑
}
}
}, "fixed-worker-2").start();
}

更健壮的方案是使用 ReentrantLock.tryLock() 设置超时,拿不到锁就回滚:
Lock lockA = new ReentrantLock();
Lock lockB = new ReentrantLock();
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
try {
// 业务逻辑
} finally { lockB.unlock(); }
} // 拿不到 lockB 自动释放 lockA
} finally { lockA.unlock(); }
}
4.2 锁竞争优化
5 个 contention-worker 抢一把公平锁,临界区耗时 5 秒。三个优化方向:
① 缩小临界区 → 将耗时操作移出锁范围
// 修复前
fairLock.lock();
try {
String data = fetchFromDb(); // 5s
process(data);
} finally { fairLock.unlock(); }
// 修复后
String data = fetchFromDb(); // 不加锁
fairLock.lock();
try {
process(data); // 只锁必要代码
} finally { fairLock.unlock(); }
② 非公平锁替代公平锁 → 公平锁吞吐量更低(线程唤醒 + 上下文切换开销),非公平锁减少 30-50% 切换损耗:
private static final Lock lock = new ReentrantLock(false); // 默认非公平
③ 乐观锁替代 → 如果竞争的是计数器或状态标记,用 AtomicInteger 或 LongAdder:
private final AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 无锁 CAS,不阻塞
4.3 CPU 优化
cpu-cruncher 线程死循环重复计算 Math.sin(i) * Math.cos(i),不缓存、不降频,单核跑满。修复:加频率限制:
// 修复前
while (true) {
double x = 0;
for (int i = 0; i < 1_000_000; i++) x += Math.sin(i) * Math.cos(i);
}
// 修复后
while (true) {
double x = 0;
for (int i = 0; i < 1_000_000; i++) x += Math.sin(i) * Math.cos(i);
Thread.sleep(50); // 每秒最多 20 次,释放 CPU
}
生产环境中类似问题排查思路:top -H 定位高 CPU 的 nid → Dump 中搜 0xnid → 定位到代码行 → 评估能否加缓存、降频率或用更高效算法。
5. 验证结果
5.1 死锁修复验证
连续取 3 次 Dump,deadlock 检测输出消失:
$ for i in 1 2 3; do jstack <pid> | grep -c 'deadlock'; done
0
0
0
5.2 锁竞争验证
BLOCKED 线程数从 5 降至 0:
$ grep -c 'BLOCKED' /tmp/dump-*.log
dump-1.log:0
dump-2.log:0
dump-3.log:0
线程状态分布回归健康:
$ jstack <pid> | grep 'java.lang.Thread.State' | sort | uniq -c | sort -rn
22 java.lang.Thread.State: TIMED_WAITING (parking)
8 java.lang.Thread.State: TIMED_WAITING (sleeping)
6 java.lang.Thread.State: RUNNABLE
4 java.lang.Thread.State: WAITING (parking)
1 java.lang.Thread.State: WAITING (on object monitor)
BLOCKED = 0,恢复正常。
5.3 CPU 验证
$ top -b -n 1 | head -20
top - 09:35:22 up 12 days, 3:48, 3 users, load average: 2.1, 8.3, 7.9
%Cpu(s): 12.5 us, 2.3 sy, 0.0 ni, 82.1 id, 0.0 wa, 0.0 hi, 3.1 si, 0.0 st
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
34512 appuser 20 0 4654824 785240 15232 S 28.1 4.9 25:12.15 java
CPU idle 从 5.2% 恢复到 82.1%,load average 从 18.32 降到 2.1。
附录:线程状态速查表
| 状态 | JVM 定义 | 含义 | 排查方向 |
|---|---|---|---|
| BLOCKED | BLOCKED (on object monitor) |
等待进入 synchronized 块 | 找锁持有者、分析临界区大小 |
| WAITING | WAITING (on object monitor) |
调用了 Object.wait() 等通知 | 看是否缺 notify |
| WAITING | WAITING (parking) |
调用了 LockSupport.park() | JUC 锁等待,看 AQS 队列 |
| TIMED_WAITING | TIMED_WAITING (sleeping) |
Thread.sleep() | 正常,数量过多可能有设计问题 |
| TIMED_WAITING | TIMED_WAITING (parking) |
LockSupport.parkNanos() | 连接池/线程池空闲等待 |
| RUNNABLE | RUNNABLE |
可运行,不阻塞 | 可能是 CPU 计算或 IO 操作 |
| NEW | - | 线程已创建但未 start | 一般正常 |
| TERMINATED | - | 线程已结束 | 一般正常 |
6. 避坑建议
6.1 线程 Dump 三板斧
拿到 Dump 后的操作顺序:
- 扫头 30 行:看有没有
Found one Java-level deadlock(JVM 自动检测) - 查 BLOCKED 数量:正常应为 0,>3 就是异常
- 逐线程读栈:找业务代码包名,看每个线程卡在哪
6.2 多次 Dump 对比
单次 Dump 永远不够。至少取 3 次(间隔 5s):
for i in 1 2 3; do
jstack <pid> > dump-${i}.log
sleep 5
done
对比三次的差异: - 同一个线程一直 RUNNABLE 且栈不变 → 可能在死循环或长时间运算 - BLOCKED 数量持续不降 → 锁竞争严重 - WAITING 线程一直不醒 → 可能缺 notify
6.3 线程名就是第一手线索
Demo 中通过 new Thread("contention-worker-3") 设置了有意义的线程名。生产环境务必给线程取好名字——这能在 Dump 中节约 80% 的定位时间。
6.4 死锁预防
1. 避免嵌套锁:两个以上锁的获取顺序必须全局一致
2. 使用 tryLock 并设置超时:拿不到锁就回滚
3. 加锁范围要小:只锁必要代码,不要锁整段方法
6.5 配合 OS 级工具
| 工具 | 用途 |
|---|---|
top -H -p <pid> |
看线程级 CPU 消耗(对应 nid) |
cat /proc/<pid>/status |
看线程数、上下文切换次数 |
vmstat 1 |
看系统整体运行队列 |
pidstat -t -p <pid> 1 |
每个线程的 CPU 和时间 |
7. 附:完整命令清单
进程定位
top -b -n 1 | head -20 # 查看 CPU/内存/Java 进程
ps aux | grep java # 找 Java 进程 PID
top -H -p <pid> # 看线程级 CPU 消耗
线程 Dump 采集
jstack <pid> # 采集一次线程 Dump
jstack <pid> > /tmp/threaddump-$(date +%s).log # 保存到文件
kill -3 <pid> # 另一种方式触发 Dump(输出到 stdout)
for i in 1 2 3; do jstack <pid> > dump-$i.log; sleep 5; done # 连续采集 3 次
状态分析
grep 'java.lang.Thread.State' dump.log | sort | uniq -c | sort -rn # 状态分布统计
grep -c 'BLOCKED' dump-*.log # BLOCKED 数量
grep -A 30 'deadlock' dump.log # 看死锁详情
grep -A 10 'BLOCKED' dump.log | grep 'your.package' # 找业务代码 BLOCKED
线程定位(CPU 对应)
# 1. top -H 中找到高 CPU 的 nid(十六进制)
# 2. Dump 中搜该 nid
printf '%x\n' <nid十进制> # 十进制转十六进制
grep '0x<p十六进制>' dump.log -A 15 # 找对应线程栈
锁分析
grep 'locked' dump.log # 持有锁的线程
grep 'waiting to lock' dump.log # 等锁的线程
grep -o '0x[0-9a-f]\{16\}' dump.log | sort | uniq -c | sort -rn # 锁地址热力图
系统辅助
cat /proc/<pid>/status | grep -E 'Threads|voluntary|nonvoluntary' # 线程数 + 上下文切换
vmstat 1 5 # 系统运行指标
📖 完整版带可复现 Demo -> opencao.cn 📺 公众号「Ai拆代码的曹操」 🌟 知识星球「源阅会」(82877104)
复现环境:JDK 17.0.1 / -Xmx512m 复现时间:2026-06-18 09:32 ~ 09:35 截图生成工具:
tools/server-mockup.html+tools/chat-mockup.html+tools/code-mockup.html