📋 概述
一句话:线上 CPU 突然飙到 100%,接口响应从 50ms 变成 5s,用户疯狂投诉------怎么在 10 分钟内定位是哪个线程在捣乱?
这是每个 Java 后端工程师迟早会遇到的噩梦。并发问题是最难排查的线上问题之一,原因有三:
- 不可复现:同样的代码,测试环境跑一万次没问题,线上跑一次就死锁。
- 现象模糊:CPU 100%、响应变慢、数据不一致------到底是死锁、线程泄漏还是 GC?
- 工具门槛高:jstack、jconsole、arthas......工具多但不知道什么时候用哪个。
本篇用问题驱动 + 生活类比 + 流程图 + 可跑代码 的方式,把并发问题排查从"看到 CPU 100% 就慌"升级为"10 分钟定位根因"。覆盖 5 类高频并发问题:死锁、CPU 飙高、线程泄漏、OOM 关联线程、任务丢失,每类给排查步骤 + 实战命令 + demo 代码。
💡 生活类比:急诊分诊 + 侦探破案
并发问题排查 = 急诊分诊 + 侦探破案,分三步走:

| 步骤 | 急诊类比 | 侦探类比 | 对应操作 |
|---|---|---|---|
| 分诊 | 看体温、血压、心率 | 了解案发现场 | top 看 CPU/内存,jps 确认进程 |
| 锁定嫌疑人 | 哪个器官出问题 | 用监控录像缩小范围 | top -Hp 找问题线程,jstack 导出栈 |
| 取证定罪 | 检查化验单 | 分析指纹、时间线 | 分析线程状态、锁持有关系、等待链 |
| 对症下药 | 开药/手术 | 抓捕嫌疑人 | 修复代码、调整参数、加监控 |
核心心法:先看"生命体征"(CPU/内存/线程数),再定位"嫌疑人"(具体线程),最后"取证"(线程栈分析)。不要上来就看日志,那相当于不量体温就开处方。
🔍 并发问题分类

| 问题类型 | 根本原因 | 典型症状 | 排查工具 | 难度 |
|---|---|---|---|---|
| 死锁 | 两把锁交叉持有 | 应用卡死、CPU 正常、日志无异常 | jstack、jconsole、arthas | ⭐⭐⭐ |
| CPU 飙高 | 死循环、自旋、正则回溯 | CPU 100%、接口超时、GC 日志正常 | top + jstack、arthas thread | ⭐⭐⭐⭐ |
| 线程泄漏 | 线程池未关闭、线程未回收 | 线程数持续增长、最终 OOM | jstack、jmap、jconsole | ⭐⭐⭐ |
| OOM 关联线程 | 递归过深/线程数超限 | StackOverflowError 或 unable to create native thread | jstack、jmap + MAT | ⭐⭐ |
| 任务丢失 | 异常吞掉、拒绝策略不当 | 业务数据丢失、无报错 | 代码审查、arthas watch | ⭐⭐ |
🔍 死锁排查 ⭐⭐
死锁的 4 个必要条件
死锁(Deadlock)必须同时满足以下 4 个条件,缺一不可:
| 条件 | 含义 | 生活类比 |
|---|---|---|
| 互斥 | 资源一次只能被一个线程持有 | 厕所一次只能一个人用 |
| 占有并等待 | 持有资源的线程还在等另一个资源 | 拿着 A 号等 B 号 |
| 不可抢占 | 不能强制释放已持有的资源 | 不能把正在上厕所的人拽出来 |
| 循环等待 | 线程之间形成等待环 | A 等 B、B 等 A |
制造死锁的 Demo
java
public class DeadLockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread-1: 持有 lockA,等待 lockB...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) {
System.out.println("Thread-1: 拿到 lockB");
}
}
}, "Thread-1");
Thread t2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread-2: 持有 lockB,等待 lockA...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockA) {
System.out.println("Thread-2: 拿到 lockA");
}
}
}, "Thread-2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("程序结束");
}
}
运行后程序永远卡住------两个线程互相等待对方释放锁,永远不会结束。
排查步骤

方式一:jstack
perl
# 1. 找到 Java 进程 pid
jps -l
# 输出示例:
# 12345 com.example.DeadLockDemo
# 2. 导出线程栈
jstack 12345 > thread_dump.txt
# 3. 搜索 deadlock 关键字
grep -A 20 "Found one Java-level deadlock" thread_dump.txt
输出示例:
vbnet
Found one Java-level deadlock:
===============================
"Thread-2":
waiting to lock monitor 0x00007f8b5c003818 (object 0x00000007aab4a200, a java.lang.Object),
which is held by "Thread-1"
"Thread-1":
waiting to lock monitor 0x00007f8b5c006218 (object 0x00000007aab4a210, a java.lang.Object),
which is held by "Thread-2"
Java level deadlock details:
=============================
"Thread-2":
at com.example.DeadLockDemo.lambda$main$1(DeadLockDemo.java:18)
- waiting to lock <0x00000007aab4a200> (a java.lang.Object)
- locked <0x00000007aab4a210> (a java.lang.Object)
"Thread-1":
at com.example.DeadLockDemo.lambda$main$0(DeadLockDemo.java:11)
- waiting to lock <0x00000007aab4a210> (a java.lang.Object)
- locked <0x00000007aab4a200> (a java.lang.Object)
方式二:jconsole
shell
# 启动 jconsole 连接到目标进程
jconsole <pid>
# → 选 "Thread" 标签
# → 点击 "Detect Deadlock" 按钮
# → 自动列出死锁线程及锁对象
方式三:arthas
shell
# 1. 启动 arthas 并连接
java -jar arthas-boot.jar
# 2. 一条命令定位死锁
thread -b
# 输出示例:
# "Thread-2" Id=12 BLOCKED on java.lang.Object@1a2b3c owned by "Thread-1" Id=11
# at com.example.DeadLockDemo.lambda$main$1(DeadLockDemo.java:18)
# - blocked on java.lang.Object@1a2b3c
# - locked java.lang.Object@4d5e6f
死锁修复方案
| 方案 | 做法 | 适用场景 |
|---|---|---|
| 按序加锁 | 所有线程按固定顺序获取锁(A→B) | 简单场景,锁数量少 |
| tryLock 超时 | ReentrantLock.tryLock(timeout) + 失败回滚 |
多锁场景,需要容错 |
| 锁粒度细化 | 减小锁范围,降低交叉概率 | 复杂业务逻辑 |
| 死锁检测+自动恢复 | Timer 定期 jstack 检测,发现死锁强制释放 | 高可用系统兜底 |
❌ 错误做法:用 Thread.sleep() 假装"等一等"来避免死锁------这不是解决方案,只是降低了碰撞概率。
✅ 正确做法:从设计层面消除循环等待------所有线程按同一顺序获取锁。
🔍 CPU 飙高排查 ⭐⭐
问题场景
应用 CPU 突然飙到 100%,接口响应变慢,但没有 OOM、没有死锁日志。常见原因:
- 死循环(如 while 条件永远为 true)
- 正则表达式回溯(灾难性回溯)
- 频繁 Full GC(GC 线程占满 CPU)
- 序列化/反序列化死循环
- 线程自旋过多
排查步骤

完整 Bash 命令流程
perl
# 第一步:找出 CPU 最高的 Java 进程
top
# 记录 PID,比如 12345
# 第二步:找出该进程中 CPU 最高的线程
top -Hp 12345
# 记录 CPU 最高的线程 tid,比如 12367
# 第三步:把 tid 转成 16 进制
printf "%x\n" 12367
# 输出:304f
# 第四步:在线程栈中搜索该 16 进制 nid
jstack 12345 | grep "304f" -A 30
# 输出示例:
# "pool-1-thread-3" Id=13 nid=0x304f RUNNABLE
# at com.example.BusyLoop.process(BusyLoop.java:25)
# at com.example.BusyLoop.run(BusyLoop.java:15)
方式二:arthas 一条命令
shell
# 找出 CPU 使用率最高的 3 个线程
thread -n 3
# 输出示例:
# "pool-1-thread-3" Id=13 cpuUsage=95.2% RUNNABLE
# at com.example.BusyLoop.process(BusyLoop.java:25)
# 直接反编译看源码
jad com.example.BusyLoop
制造 CPU 飙高的 Demo
arduino
public class CpuHighDemo {
public static void main(String[] args) {
// 故意制造死循环
new Thread(() -> {
int i = 0;
while (true) { // 条件永远为 true,CPU 空转
i++;
}
}, "BusyThread").start();
System.out.println("主线程运行中,BusyThread 在后台空转");
}
}
jstack 线程状态统计
拿到 jstack dump 后,先做线程状态统计,快速判断问题类型:
bash
# 统计各状态线程数
jstack 12345 | grep "java.lang.Thread.State" | sort | uniq -c | sort -rn
# 输出示例:
# 45 RUNNABLE ← 有 45 个线程在运行,可能 CPU 飙高
# 12 WAITING ← 等待中
# 8 TIMED_WAITING ← 超时等待
# 3 BLOCKED ← 被阻塞,可能死锁
| 状态 | 含义 | 对应问题 |
|---|---|---|
| RUNNABLE 数量异常多 | 大量线程在运行 | CPU 飙高、死循环、自旋 |
| BLOCKED 数量 > 0 | 有线程被阻塞 | 死锁、锁竞争激烈 |
| WAITING 数量异常多 | 大量线程在等待 | 线程池队列积压、wait/notify 配对问题 |
🔍 线程泄漏排查
问题场景
应用运行一段时间后,线程数从 50 涨到 500、5000......最终抛出 OutOfMemoryError: unable to create new native thread。
常见原因
| 原因 | 代码示例 | 后果 |
|---|---|---|
| 线程池未关闭 | newFixedThreadPool() 用完不 shutdown |
线程一直存活 |
| 线程没有回收 | new Thread().start() 后不 join |
守护线程泄漏 |
| ThreadLocal 泄漏 | 线程池中使用 ThreadLocal 不 remove | 线程复用导致内存泄漏 |
| 自定义线程没设 daemon | 后台线程不设为守护线程 | JVM 无法退出 |
排查步骤
bash
# 1. 查看当前 Java 进程线程数
jstack 12345 | grep -c ""
# 或更精确地:
jstack 12345 | grep "^"" | wc -l
# 2. 按线程名分组统计
jstack 12345 | grep "^"" | sed 's/".*//' | sort | uniq -c | sort -rn | head -20
# 输出示例:
# 200 "pool-3-thread-"
# 50 "http-nio-8080-exec-"
# 10 "main"
# → pool-3-thread 有 200 个!说明某个线程池在泄漏
# 3. 看这些线程在干什么
jstack 12345 | grep -A 5 "pool-3-thread-1"
# 4. 用 arthas 实时监控线程数变化
thread | wc -l
线程泄漏 Demo
java
public class ThreadLeakDemo {
private static final ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws Exception {
for (int i = 0; i < 100; i++) {
executor.submit(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// ❌ 忘记 shutdown!线程池一直存活
// executor.shutdown();
System.out.println("任务已提交,但线程池未关闭");
}
}
csharp
# 排查命令
jstack <pid> | grep "^"" | sed 's/".*//' | sort | uniq -c | sort -rn
# 10 "pool-1-thread-" ← 线程池 10 个线程一直存活
🔍 OOM 与线程
StackOverflowError:递归过深
typescript
public class StackOverflowDemo {
public static void main(String[] args) {
recursiveMethod(0);
}
private static void recursiveMethod(int depth) {
System.out.println("depth = " + depth);
recursiveMethod(depth + 1); // 无限递归
}
}
perl
# jstack 会看到大量重复的递归栈帧
jstack <pid> | grep -c "recursiveMethod"
# 输出:5000+ ← 栈帧深度爆炸
OutOfMemoryError: unable to create new native thread
当系统线程数达到 OS 限制时:
bash
# 查看系统线程数限制
ulimit -u
# 输出:4096(Linux 默认)
# 查看当前 Java 进程线程数
ls /proc/<pid>/task | wc -l
# 调大限制(临时)
ulimit -u 65535
| 错误类型 | 根因 | 排查命令 | 解决方案 |
|---|---|---|---|
| StackOverflowError | 递归无终止条件 | jstack 看重复栈帧 |
加终止条件、改循环 |
| unable to create native thread | 线程数超 OS 限制 | `ls /proc/ |
wc -l` |
| OutOfMemoryError: heap | 堆内存不足 | jmap -heap <pid> |
检查内存泄漏、调 -Xmx |
💻 可跑代码 / 命令汇总
1. 制造死锁 + jstack 排查
java
public class DeadLockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lockA) {
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lockB) { System.out.println("T1 done"); }
}
}, "Thread-1").start();
new Thread(() -> {
synchronized (lockB) {
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lockA) { System.out.println("T2 done"); }
}
}, "Thread-2").start();
}
}
perl
# 编译运行
javac DeadLockDemo.java && java DeadLockDemo
# 程序卡住,Ctrl+C 或另开终端
# 排查
jps -l # 找 pid
jstack <pid> | grep -A 20 "Found deadlock"
2. 制造 CPU 飙高 + top/jstack 排查
typescript
public class CpuHighDemo {
public static void main(String[] args) {
new Thread(() -> {
while (true) { /* 死循环,CPU 空转 */ }
}, "BusyThread").start();
}
}
perl
javac CpuHighDemo.java && java CpuHighDemo
# 排查
top -Hp <pid> # 找到 BusyThread 的 tid
printf "%x\n" <tid> # 转 16 进制
jstack <pid> | grep "<hex>" -A 30 # 定位代码行
3. 线程池泄漏 + jstack 排查
java
public class LeakDemo {
private static final ExecutorService pool = Executors.newCachedThreadPool();
public static void main(String[] args) {
for (int i = 0; i < 500; i++) {
pool.submit(() -> {
try { Thread.sleep(60000); } catch (Exception e) {}
});
}
// ❌ 忘记 shutdown
}
}
bash
javac LeakDemo.java && java LeakDemo
# 排查:线程数持续增长
jstack <pid> | grep "^"" | wc -l # 线程总数
jstack <pid> | grep "^"" | sort | uniq -c | sort -rn | head -5 # 按名分组
4. Arthas 基础命令
bash
# 启动 arthas
java -jar arthas-boot.jar
# 查看所有线程
thread
# 查看 CPU 最高的 N 个线程
thread -n 3
# 查看指定线程栈
thread <id>
# 查看死锁
thread -b
# 反编译类
jad com.example.MyClass
# 监控方法调用
watch com.example.MyClass method '{params, returnObj, throwExp}' -x 2
# 追踪方法调用链路
trace com.example.MyClass method
# 退出 arthas
quit
5. jstack 分析线程状态统计
bash
#!/bin/bash
# thread-stats.sh - jstack 线程状态统计脚本
PID=$1
if [ -z "$PID" ]; then
echo "Usage: $0 <pid>"
exit 1
fi
echo "=== 线程总数 ==="
jstack $PID | grep "^"" | wc -l
echo ""
echo "=== 线程状态分布 ==="
jstack $PID | grep "java.lang.Thread.State" | sort | uniq -c | sort -rn
echo ""
echo "=== 按线程名分组 TOP10 ==="
jstack $PID | grep "^"" | cut -d'"' -f2 | sed 's/-[0-9]*$//' | sort | uniq -c | sort -rn | head -10
echo ""
echo "=== BLOCKED 线程详情 ==="
jstack $PID | grep -B 2 "BLOCKED" | head -20
arduino
# 使用方式
bash thread-stats.sh 12345
⚠️ 常见问题与踩坑
Q1:死锁一定有 "Found one Java-level deadlock" 吗?
不一定。 jstack 能检测的是经典的 Java 级死锁(两把锁交叉持有)。以下情况 jstack 不报死锁:
| 情况 | 说明 | 如何排查 |
|---|---|---|
| 活锁 | 线程不断重试但永远失败(如 CAS 自旋) | 看 CPU 是否飙高,线程栈是否重复 |
| 饥饿 | 低优先级线程永远拿不到锁 | 看线程状态是否长期 BLOCKED/WAITING |
| 数据库死锁 | SQL 层面的死锁 | 看数据库日志,不在 jstack 体现 |
| 线程池队列满 | 任务提交后永远不执行 | 看线程池状态、拒绝策略 |
Q2:jstack 无输出或 "nid" 找不到怎么办?
可能原因及解决:
bash
# 1. 权限不足:需要用同用户执行
# 错误:jstack 12345 → Permission denied
# 解决:sudo -u <java_user> jstack 12345
# 2. 进程不存在:确认 pid 正确
jps -l
# 3. jstack 版本与 JDK 不匹配
java -version # 确认 java 版本
which jstack # 确认 jstack 路径一致
# 4. 线程已结束:dump 时线程已经退出
# 解决:循环多次 jstack 对比
for i in {1..5}; do jstack 12345 > dump_$i.txt; sleep 1; done
Q3:arthas 怎么安装和启动?
bash
# 方式一:直接下载 jar 运行
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
# 方式二:通过arthas-agent attach 到运行中的 JVM
# 在应用启动时加入
-javaagent:arthas-agent.jar
# 方式三:Docker 容器内
# 进入容器后下载并运行
docker exec -it <container> bash
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
Q4:线上能直接 jstack 吗?有性能影响吗?
有影响,但通常可接受。
| 操作 | 性能影响 | 说明 |
|---|---|---|
jstack <pid> |
低(STW 几毫秒) | 只 dump 线程栈,不停应用 |
jstack -F <pid> |
中(可能 STW 几十毫秒) | force 模式,用 serviceability agent |
jmap -dump |
高(STW 几秒到分钟) | dump 堆,生产慎用 |
arthas thread |
低 | 底层也是 jstack |
最佳实践:
- jstack 在生产环境可以使用,建议在低峰期
- 避免频繁执行(每分钟不超过 1 次)
- 使用
jstack <pid> > dump.txt重定向到文件,不要直接看控制台 - 需要多次 dump 时,间隔 > 5 秒
Q5:如何监控线程数并设置告警?
ini
# 方式一:jstack 定期统计
# crontab 每分钟执行
*/1 * * * * jstack <pid> | grep "^"" | wc -l >> /tmp/thread_count.log
# 方式二:JMX 监控(推荐)
# 在 Java 启用 JMX
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
# 方式三:Micrometer + Prometheus(生产推荐)
# 在 Spring Boot 中
# management.endpoints.web.exposure.include=metrics
# 访问 /actuator/metrics/jvm.threads.live
Q6:arthas watch/trace 命令输出太多怎么办?
perl
# watch:限制输出次数
watch com.example.MyClass method '{params}' -x 2 #10
# trace:只看耗时 > 100ms 的调用
trace com.example.MyClass method '#cost > 100'
# trace:只看前 5 层调用
trace com.example.MyClass method -n 5
# 清除所有增强
reset
🎯 最佳实践
排查流程图(完整版)

工具选型表
| 工具 | 适用场景 | 优势 | 劣势 | 安装方式 |
|---|---|---|---|---|
| jstack | 死锁、线程状态分析 | JDK 自带,无依赖 | 静态快照,无实时性 | JDK 自带 |
| jconsole | 死锁检测、线程监控 | GUI 可视化,一键检测 | 需要图形界面 | JDK 自带 |
| arthas | 实时排查、方法级追踪 | 动态增强,不重启 | 学习曲线较陡 | 需下载 jar |
| jmap + MAT | 内存泄漏、OOM | 堆分析权威 | dump 文件大,耗时 | MAT 需额外安装 |
| VisualVM | 综合监控、线程/内存 | 可视化好 | 性能一般 | JDK 自带 |
| async-profiler | CPU 火焰图 | 低开销,生产可用 | 需要额外部署 | 需下载 |
监控建议
| 监控项 | 告警阈值 | 推荐工具 |
|---|---|---|
| 线程数 | > 500(根据业务调整) | Micrometer / JMX |
| BLOCKED 线程数 | > 0 持续 5 分钟 | jstack 定期 dump |
| CPU 使用率 | > 80% 持续 3 分钟 | Prometheus + Grafana |
| 死锁检测 | 每 5 分钟 jstack 自动检测 | 自定义脚本 + 告警 |
| 线程池队列 | 队列积压 > 100 | 自定义指标上报 |
检查清单
- 生产环境有 jstack/arthas 快速接入能力
- 线程数监控已配置,有告警
- 线程池都有名称(便于 jstack 识别)
- 多锁场景使用 tryLock 超时防死锁
- 所有锁按固定顺序获取
- 线程池使用后正确 shutdown
- ThreadLocal 在线程池中使用后 remove
💡 面试要点
- 线上 CPU 100% 怎么排查? →
top -Hp <pid>找线程 →printf "%x"转 16 进制 →jstack <pid> | grep <hex>定位代码行。面试时能说出完整 bash 命令流程,比只说"用 jstack"有区分度。 - jstack 能检测所有死锁吗? → 不能。jstack 只能检测 Java 级别的经典死锁(两把锁交叉持有)。数据库死锁、活锁、饥饿、线程池队列满等不在其检测范围。
- 死锁的 4 个必要条件是什么? → 互斥、占有并等待、不可抢占、循环等待。破坏任一条件即可预防死锁。最常见的方案是按序加锁 (破坏循环等待)和 tryLock 超时(破坏不可抢占)。
- 线程泄漏怎么排查? →
jstack | grep "^"" | sort | uniq -c | sort -rn按线程名分组统计,找出数量异常的线程池。常见原因:线程池未 shutdown、ThreadLocal 未 remove、未设 daemon。 - jstack 和 arthas thread 有什么区别? → jstack 是 JDK 自带,获取静态快照;arthas thread 底层也是 jstack,但支持
-n N找 CPU 最高线程、-b检测死锁、实时监控等高级功能。 - 线上能直接 jstack 吗? → 可以,jstack 性能影响很低(STW 几毫秒)。但避免频繁执行(每分钟 ≤ 1 次),建议输出到文件而非控制台。
- arthas 怎么用? →
java -jar arthas-boot.jar启动 →thread -b检测死锁 →thread -n 3找 CPU 最高线程 →jad反编译 →watch/trace方法级追踪。 - 如何从设计上预防死锁? → ① 所有线程按固定顺序获取锁(破坏循环等待);② 使用
tryLock(timeout)超时放弃(破坏不可抢占);③ 减小锁粒度降低交叉概率。
📝 总结
| 要点 | 记住这一句 |
|---|---|
| 排查心法 | 先分诊(看大盘)→ 锁嫌疑人(定位线程)→ 取证(分析栈)→ 下药(修复) |
| CPU 飙高 | top -Hp → printf "%x" → jstack,三步定位代码行 |
| 死锁检测 | jstack 看 "Found deadlock",arthas thread -b 一条命令 |
| 线程泄漏 | jstack 按名分组统计,找数量异常的线程池 |
| 工具选型 | 静态分析用 jstack,实时追踪用 arthas,堆分析用 MAT |
| 预防死锁 | 按序加锁 + tryLock 超时 + 锁粒度最小化 |
| 监控先行 | 线程数、BLOCKED 数、CPU 使用率,三大指标必须有告警 |
📖 外部参考
- 📖 jstack 官方文档 --- JDK 17 jstack 工具说明
- 📖 Arthas 官方文档 --- 阿里巴巴开源 Java 诊断工具
- 📖 Java Concurrency in Practice --- Brian Goetz --- 第 12 章并发程序的测试与调试
- 📖 OpenJDK Thread Dump Format --- 线程 dump 格式规范
- 📖 MAT (Memory Analyzer Tool) --- 堆内存分析权威工具