线程 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

top 定位 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)

jstack 采集与统计

关键信号: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 线程长这样:

线程 Dump 全貌

解读模板(一行拆解):

"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

nonvoluntaryvoluntary 高了 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-1deadlock-worker-2 以不同顺序获取 lockAlockB统一锁获取顺序即可消除循环等待:

// 修复前: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();
}

死锁修复代码对比:统一锁顺序 + tryLock

更健壮的方案是使用 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); // 默认非公平

③ 乐观锁替代 → 如果竞争的是计数器或状态标记,用 AtomicIntegerLongAdder

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 后的操作顺序:

  1. 扫头 30 行:看有没有 Found one Java-level deadlock(JVM 自动检测)
  2. 查 BLOCKED 数量:正常应为 0,>3 就是异常
  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