死锁排查与性能调优:线上并发问题诊断指南
作者 :Weisian
发布时间:2026年3月

直击痛点:
"线上系统突然卡死,所有请求都超时,你慌了;CPU飙到200%,服务器报警,你只知道重启;面试官问'怎么排查死锁',你说'用jstack',然后呢?怎么定位哪个线程死锁?怎么找代码位置?'"
在Java并发编程的世界里,死锁和性能问题是最棘手的线上故障:
- 死锁:两个线程互相等待对方释放资源,双双"僵死",系统瘫痪;
- CPU飙升:某个线程死循环或频繁自旋,把CPU跑满,其他任务排队等死;
- 内存溢出:线程数过多或对象未释放,OOM导致服务崩溃;
- 面试高频问:"死锁的四个必要条件是什么?""如何用jstack定位死锁?""CPU 100%怎么排查?""Arthas的thread命令怎么用?"------答不上来=错失高薪offer。
本文将从实战场景 切入,结合工具链 、排查步骤 、案例分析 ,彻底讲透死锁排查与性能调优的全流程:
✅ 死锁原理:四个必要条件 + 转账案例复现;
✅ 死锁预防:固定顺序获取锁、定时尝试锁;
✅ 死锁检测:jps、jstack、jconsole、jvisualvm实战;
✅ Arthas在线诊断:thread命令、watch命令、热更新;
✅ CPU飙升排查:top → ps → jstack 三板斧;
✅ 内存溢出排查:jmap、jstat、MAT分析;
✅ 性能调优三板斧:减少锁粒度、缩短锁持有时间、无锁数据结构;
✅ 实战演练:模拟死锁并使用工具完整排查;
✅ 面试高频真题标准答案(直接背)。
📌 核心一句话 :
死锁是线程互相僵持、谁都不放手 的僵局,必须同时满足4个条件;排查死锁用
jstack/Arthas,排查CPU飙升用top+jstack+火焰图;预防死锁核心是打破任意一个必要条件 ,性能调优核心是少加锁、快放锁、用无锁。
📌 排查金句先记牢:
死锁四要素:互斥、请求保持、不可剥夺、循环等待,缺一不可;
排查死锁三步走:
jps找进程 →jstack查线程栈 → 搜索deadlock关键字;CPU 100%排查四步:
top定进程 →top -H定线程 → 转16进制 →jstack查代码;Arthas一键命令:
thread查死锁,thread -n 3查CPU最高线程,profiler生成火焰图;打破循环等待是最实用的死锁预防方案:固定锁的获取顺序;
性能调优核心:锁的粒度越小越好,持有锁的时间越短越好。
📌 生活类比先记牢:死锁:两个人过独木桥,你等我让,我等你让,谁也过不去。
CPU飙升:一个人发疯一样原地转圈(死循环),把路占满,别人都走不动。
线程阻塞:你在门口等快递,快递员一直不来,你就一直等着。
锁粒度:整个图书馆一次只能进一个人(粗粒度)vs 每个书架可以单独借阅(细粒度)。
一、死锁原理:从理论到复现
1.1 死锁的四个必要条件
生活化类比 :
两个人(线程A、线程B)去餐厅吃饭,都需要筷子+碗才能吃饭:
- 线程A抢到了筷子 (持有锁1),等着要碗(请求锁2);
- 线程B抢到了碗 (持有锁2),等着要筷子(请求锁1);
- 双方都不肯放手自己的资源,也拿不到对方的资源,永远僵持------这就是死锁。
四个必要条件(缺一不可):
- 互斥条件:资源不能被共享,一次只能一个线程使用;
- 请求与保持:线程已经持有了一个资源,又在等待另一个资源,且不释放已持有的;
- 不可剥夺条件:已经获得的资源,在未使用完之前不能被强行剥夺;
- 循环等待条件:线程A等B,B等C,C等A,形成环路。

1.2 死锁代码复现
java
/**
* 经典死锁场景:两个线程互相持有对方需要的锁
*/
public class DeadlockDemo {
// 两个锁对象
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
public static void main(String[] args) {
// 线程1:先拿LOCK_A,再拿LOCK_B
Thread thread1 = new Thread(() -> {
synchronized (LOCK_A) {
System.out.println("Thread1: 获得锁A");
try {
Thread.sleep(100); // 模拟业务处理
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread1: 等待锁B");
synchronized (LOCK_B) {
System.out.println("Thread1: 获得锁B");
}
}
}, "Thread-1");
// 线程2:先拿LOCK_B,再拿LOCK_A
Thread thread2 = new Thread(() -> {
synchronized (LOCK_B) {
System.out.println("Thread2: 获得锁B");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread2: 等待锁A");
synchronized (LOCK_A) {
System.out.println("Thread2: 获得锁A");
}
}
}, "Thread-2");
thread1.start();
thread2.start();
// 主线程等待一段时间后退出
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("程序结束,但实际线程还在死锁中...");
}
}
运行结果:
Thread1: 获得锁A
Thread2: 获得锁B
Thread1: 等待锁B
Thread2: 等待锁A
程序结束,但实际线程还在死锁中...
1.3 实战案例:转账场景死锁
java
/**
* 转账场景死锁:两个账户同时互相转账
*/
public class TransferDeadlockDemo {
static class Account {
private final String name;
private int balance;
public Account(String name, int balance) {
this.name = name;
this.balance = balance;
}
public void transfer(Account target, int amount) {
// 先锁自己,再锁对方(死锁风险)
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " 锁定账户: " + this.name);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (target) {
System.out.println(Thread.currentThread().getName() + " 锁定账户: " + target.name);
if (this.balance >= amount) {
this.balance -= amount;
target.balance += amount;
System.out.println(Thread.currentThread().getName() + " 转账成功: " + amount);
} else {
System.out.println(Thread.currentThread().getName() + " 余额不足");
}
}
}
}
@Override
public String toString() {
return "Account{name='" + name + "', balance=" + balance + "}";
}
}
public static void main(String[] args) throws InterruptedException {
Account accountA = new Account("A", 1000);
Account accountB = new Account("B", 1000);
// 线程1:A转给B 100元
Thread t1 = new Thread(() -> accountA.transfer(accountB, 100), "转账线程-A→B");
// 线程2:B转给A 100元
Thread t2 = new Thread(() -> accountB.transfer(accountA, 100), "转账线程-B→A");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终结果: " + accountA + ", " + accountB);
}
}

二、死锁预防与避免策略
2.1 策略一:固定顺序获取锁(最常用)
核心思想:所有线程按照相同的顺序获取锁,破坏循环等待条件。
java
/**
* 转账场景:按账户ID排序,避免死锁
*/
public class SafeTransferDemo {
static class Account {
private final int id; // 唯一标识
private final String name;
private int balance;
public Account(int id, String name, int balance) {
this.id = id;
this.name = name;
this.balance = balance;
}
/**
* 安全转账:按ID排序获取锁
*/
public void transfer(Account target, int amount) {
Account first = this.id < target.id ? this : target;
Account second = this.id < target.id ? target : this;
// 先锁ID小的,再锁ID大的
synchronized (first) {
System.out.println(Thread.currentThread().getName() + " 锁定账户: " + first.name);
synchronized (second) {
System.out.println(Thread.currentThread().getName() + " 锁定账户: " + second.name);
if (this.balance >= amount) {
this.balance -= amount;
target.balance += amount;
System.out.println(Thread.currentThread().getName() + " 转账成功: " + amount);
} else {
System.out.println(Thread.currentThread().getName() + " 余额不足");
}
}
}
}
@Override
public String toString() {
return "Account{name='" + name + "', balance=" + balance + "}";
}
}
public static void main(String[] args) throws InterruptedException {
Account accountA = new Account(1, "A", 1000);
Account accountB = new Account(2, "B", 1000);
Thread t1 = new Thread(() -> accountA.transfer(accountB, 100), "转账线程-A→B");
Thread t2 = new Thread(() -> accountB.transfer(accountA, 100), "转账线程-B→A");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终结果: " + accountA + ", " + accountB);
}
}

2.2 策略二:定时尝试锁(tryLock)
核心思想 :使用ReentrantLock的tryLock方法,获取不到锁就释放已持有的锁。
java
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
/**
* 使用tryLock避免死锁
*/
public class TryLockDemo {
static class Account {
private final String name;
private int balance;
private final ReentrantLock lock = new ReentrantLock();
public Account(String name, int balance) {
this.name = name;
this.balance = balance;
}
/**
* 安全转账:使用tryLock,超时则释放锁
*/
public boolean transfer(Account target, int amount, long timeout) throws InterruptedException {
long deadline = System.currentTimeMillis() + timeout;
// 尝试获取自己的锁
while (true) {
if (this.lock.tryLock(timeout, TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 获得自己锁: " + this.name);
// 尝试获取对方的锁
long remaining = deadline - System.currentTimeMillis();
if (target.lock.tryLock(remaining, TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 获得对方锁: " + target.name);
if (this.balance >= amount) {
this.balance -= amount;
target.balance += amount;
System.out.println(Thread.currentThread().getName() + " 转账成功: " + amount);
return true;
} else {
System.out.println(Thread.currentThread().getName() + " 余额不足");
return false;
}
} finally {
target.lock.unlock();
}
}
} finally {
this.lock.unlock();
}
}
// 等待一段时间后重试
Thread.sleep(10);
}
}
@Override
public String toString() {
return "Account{name='" + name + "', balance=" + balance + "}";
}
}
public static void main(String[] args) throws InterruptedException {
Account accountA = new Account("A", 1000);
Account accountB = new Account("B", 1000);
Thread t1 = new Thread(() -> {
try {
accountA.transfer(accountB, 100, 3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "转账线程-A→B");
Thread t2 = new Thread(() -> {
try {
accountB.transfer(accountA, 100, 3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "转账线程-B→A");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终结果: " + accountA + ", " + accountB);
}
}

2.3 策略三:减少锁粒度
核心思想:用细粒度锁替代粗粒度锁,降低锁竞争。
java
/**
* 细粒度锁示例:分段锁
*/
public class FineGrainedLockDemo {
/**
* 银行账户:使用分段锁
*/
static class Bank {
// 分16个锁段,每个锁保护一部分账户
private final ReentrantLock[] locks = new ReentrantLock[16];
private final int[] balances = new int[10000];
public Bank() {
for (int i = 0; i < locks.length; i++) {
locks[i] = new ReentrantLock();
}
}
/**
* 获取账户对应的锁(按ID取模)
*/
private ReentrantLock getLock(int accountId) {
return locks[accountId % locks.length];
}
/**
* 转账:只锁涉及的两个账户对应的锁段
*/
public void transfer(int fromId, int toId, int amount) {
// 按ID排序获取锁,避免死锁
int firstId = Math.min(fromId, toId);
int secondId = Math.max(fromId, toId);
ReentrantLock firstLock = getLock(firstId);
ReentrantLock secondLock = getLock(secondId);
firstLock.lock();
try {
secondLock.lock();
try {
if (balances[fromId] >= amount) {
balances[fromId] -= amount;
balances[toId] += amount;
System.out.println(Thread.currentThread().getName() + " 转账成功: " + amount);
}
} finally {
secondLock.unlock();
}
} finally {
firstLock.unlock();
}
}
}
}

三、死锁检测工具实战
3.1 jps + jstack:命令行排查

步骤1:找到进程ID
bash
# 查看所有Java进程
jps -l
# 输出示例:
# 12345 com.example.DeadlockDemo
# 67890 org.jetbrains.jps.cmdline.Launcher
步骤2:使用jstack导出线程栈
bash
# 导出线程栈到文件
jstack 12345 > deadlock.log
# 或者直接查看
jstack 12345
步骤3:分析死锁信息
bash
# jstack会自动检测死锁
jstack 12345 | grep -A 10 "deadlock"
# 输出示例:
Found one Java-level deadlock:
=============================
"Thread-2":
waiting to lock monitor 0x00007f8c5c00a500 (object 0x00000000d5f8a7f0, a java.lang.Object),
which is held by "Thread-1"
"Thread-1":
waiting to lock monitor 0x00007f8c5c00a780 (object 0x00000000d5f8a7e0, a java.lang.Object),
which is held by "Thread-2"
Java stack information for the threads listed above:
===================================================
"Thread-2":
at com.example.DeadlockDemo.lambda$main$1(DeadlockDemo.java:30)
- waiting to lock <0x00000000d5f8a7f0> (a java.lang.Object)
- locked <0x00000000d5f8a7e0> (a java.lang.Object)
"Thread-1":
at com.example.DeadlockDemo.lambda$main$0(DeadlockDemo.java:18)
- waiting to lock <0x00000000d5f8a7e0> (a java.lang.Object)
- locked <0x00000000d5f8a7f0> (a java.lang.Object)
关键信息解读:
Found one Java-level deadlock:发现死锁waiting to lock:等待的锁locked:已经持有的锁- 代码行号:直接定位到死锁位置
3.2 jconsole:图形化监控

启动方法:
bash
# 命令行启动
jconsole
# 或找到进程后连接
jconsole <pid>
死锁检测:
- 点击"线程"选项卡
- 点击"检测死锁"按钮
- 查看死锁线程的堆栈信息
3.3 jvisualvm:性能分析工具
启动方法:
bash
# 命令行启动
jvisualvm
功能:
- 线程监控:实时查看线程状态
- CPU采样:分析热点方法
- 内存分析:查看堆内存使用
3.4 Arthas:在线诊断神器
Arthas是阿里巴巴开源的Java诊断工具,无需重启即可在线排查问题。

安装与启动:
bash
# 下载arthas
curl -O https://arthas.aliyun.com/arthas-boot.jar
# 启动,选择要监控的进程
java -jar arthas-boot.jar
核心命令:
3.4.1 thread命令:查看线程状态
bash
# 查看所有线程
thread
# 查看死锁线程
thread -b
# 输出示例:
# Found one Java-level deadlock:
# =============================
# "Thread-2":
# waiting for lock 0x00000000d5f8a7f0
# which is held by "Thread-1"
# "Thread-1":
# waiting for lock 0x00000000d5f8a7e0
# which is held by "Thread-2"
# 查看最繁忙的线程(CPU占用最高)
thread -n 3
# 查看指定线程堆栈
thread <thread-id>
3.4.2 watch命令:监控方法执行
bash
# 监控方法调用,打印参数和返回值
watch com.example.TransferDeadlockDemo$Account transfer '{params, returnObj}' -x 2
# 监控方法执行时间超过100ms的调用
watch com.example.TransferDeadlockDemo$Account transfer '{params, throwExp}' '#cost>100' -x 2
3.4.3 dashboard:实时监控面板
bash
# 进入实时监控面板
dashboard
显示内容:
- CPU使用率
- 内存使用情况
- 线程状态统计
- GC情况
3.4.4 热更新代码
bash
# 反编译代码
jad com.example.DeadlockDemo > DeadlockDemo.java
# 编辑代码(修复死锁)
vim DeadlockDemo.java
# 重新编译
mc -c <classloader-hash> DeadlockDemo.java
# 热更新
redefine /path/to/DeadlockDemo.class
四、CPU飙升排查
死锁会让程序卡死,CPU 100% 会让服务慢到无法使用,常见原因:死循环、频繁GC、复杂计算、锁竞争严重。
4.1 场景:死循环导致CPU 100%
java
/**
* CPU飙升代码:死循环
*/
public class CpuHighDemo {
public static void main(String[] args) {
// 模拟CPU飙升场景
new Thread(() -> {
System.out.println("死循环线程启动...");
while (true) {
// 空循环,CPU 100%
// 模拟计算密集型任务
double d = Math.random() * Math.random();
}
}, "Cpu-Burner").start();
// 正常业务线程
new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
System.out.println("业务线程正常运行...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Biz-Thread").start();
}
}
4.2 排查步骤
步骤1:top命令找到高CPU进程
bash
# 查看系统进程,按CPU排序
top
# 输出示例:
# PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
# 12345 root 20 0 2.5g 200m 15m R 100.0 2.5 1:23.45 java
步骤2:找到进程内的高CPU线程
bash
# 查看进程内的线程,按CPU消耗排序
top -Hp 12345
# 输出示例:
# PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
# 12346 root 20 0 2.5g 200m 15m R 100.0 2.5 1:20.12 java
# 12347 root 20 0 2.5g 200m 15m S 0.0 2.5 0:00.01 java
步骤3:转换线程ID为十六进制
bash
# 12346 转十六进制
printf "%x\n" 12346
# 输出:303a
步骤4:jstack导出线程栈
bash
# 导出线程栈
jstack 12345 > thread-dump.log
# 查找十六进制线程ID对应的堆栈
grep -A 20 "0x303a" thread-dump.log
输出示例:
"Cpu-Burner" #12 prio=5 os_prio=0 tid=0x00007f8c5c00a800 nid=0x303a runnable [0x00007f8c5c00b000]
java.lang.Thread.State: RUNNABLE
at com.example.CpuHighDemo.lambda$main$0(CpuHighDemo.java:10)
at com.example.CpuHighDemo$$Lambda$1/0x0000000800c08440.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
关键信息:
- 线程状态:
RUNNABLE - 代码位置:
CpuHighDemo.java:10→ 定位到死循环

4.3 Arthas 快速方案
bash
# 查看CPU占用最高的3个线程
thread -n 3
1秒定位问题代码,比手动命令快10倍。
4.4 排查脚本
bash
#!/bin/bash
# cpu-high.sh - CPU飙升一键排查脚本
echo "=== 1. 找到高CPU进程 ==="
PID=$(top -b -n 1 | grep java | sort -k9 -r | head -1 | awk '{print $1}')
echo "高CPU进程PID: $PID"
echo "=== 2. 找到高CPU线程 ==="
TID=$(top -Hp $PID -b -n 1 | grep java | sort -k9 -r | head -1 | awk '{print $1}')
echo "高CPU线程TID: $TID"
echo "=== 3. 线程ID转十六进制 ==="
HEX=$(printf "%x" $TID)
echo "十六进制: $HEX"
echo "=== 4. 导出线程栈 ==="
jstack $PID > /tmp/jstack-$PID.log
echo "=== 5. 定位问题代码 ==="
grep -A 20 "nid=0x$HEX" /tmp/jstack-$PID.log
4.5 火焰图(Flame Graph):可视化CPU热点
对于复杂的CPU性能问题,文字堆栈太累。火焰图(Flame Graph)是全球通用的CPU性能分析标准,能直观看到哪段代码占用CPU最高。
- 横轴:采样次数(CPU占用时间,越宽越耗时)。
- 纵轴:方法调用栈(哪一个线程导致的)。
- 越宽的方块:CPU占用越高,就是性能瓶颈。
- 颜色:不同模块(通常随机分配)。
生成步骤(简版):
-
使用
async-profiler采集数据:bash./profiler.sh -d 30 -f /tmp/flame.html 12345(采集30秒,生成HTML文件)
-
浏览器打开
/tmp/flame.html。 -
看图技巧:找最宽的矩形块,那就是CPU的"热点"。点击可下钻查看调用链。
实战价值:一眼找出死循环、慢方法、频繁GC问题。

五、内存溢出与线程数过多问题
5.1 场景:线程泄漏导致OOM
java
/**
* 线程泄漏:不断创建线程,导致系统资源耗尽
*/
public class ThreadLeakDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
// 错误:不断创建线程池,但从不关闭
for (int i = 0; i < 100000; i++) {
// 每次循环都创建新的线程池,旧的未关闭
ExecutorService leakExecutor = Executors.newFixedThreadPool(10);
leakExecutor.submit(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 忘记关闭 leakExecutor,导致线程泄漏
if (i % 1000 == 0) {
System.out.println("已创建 " + i + " 个线程池");
}
}
}
}
5.2 排查步骤
步骤1:查看线程数
bash
# 查看进程的线程数
ps -eLf | grep java | wc -l
# 或使用jstack统计
jstack <pid> | grep "Thread" | wc -l
步骤2:使用jstat查看GC情况
bash
# 查看GC统计,每1秒输出一次
jstat -gcutil <pid> 1000
# 输出示例:
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 99.90 45.23 78.45 92.34 89.12 123 2.345 5 0.678 3.023
步骤3:使用jmap导出堆内存
bash
# 导出堆内存快照
jmap -dump:live,format=b,file=heap.hprof <pid>
步骤4:使用MAT分析堆内存
- 用MAT打开
heap.hprof - 查看
Leak Suspects报告 - 找到占用内存最多的对象
5.3 常用监控命令汇总
| 命令 | 作用 | 示例 |
|---|---|---|
jps |
查看Java进程 | jps -l |
jstack |
查看线程栈 | jstack <pid> |
jmap |
查看堆内存 | jmap -heap <pid> |
jstat |
查看GC统计 | jstat -gcutil <pid> 1000 |
jinfo |
查看JVM参数 | jinfo -flags <pid> |
top |
查看系统资源 | top -Hp <pid> |
arthas |
在线诊断 | thread -b |
六、性能调优三板斧

6.1 第一板斧:减少锁粒度
原则:用细粒度锁替代粗粒度锁
java
/**
* 粗粒度锁(性能差)
*/
public class CoarseLockDemo {
private final Map<String, Integer> map = new HashMap<>();
// 整个方法加锁,影响所有操作
public synchronized void put(String key, Integer value) {
map.put(key, value);
}
public synchronized Integer get(String key) {
return map.get(key);
}
}
/**
* 细粒度锁(性能好)
*/
public class FineLockDemo {
private final Map<String, Integer> map = new HashMap<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void put(String key, Integer value) {
lock.writeLock().lock();
try {
map.put(key, value);
} finally {
lock.writeLock().unlock();
}
}
public Integer get(String key) {
lock.readLock().lock();
try {
return map.get(key);
} finally {
lock.readLock().unlock();
}
}
}
6.2 第二板斧:避免长时间持有锁
原则:只在必要时加锁
java
/**
* 锁持有时间过长(性能差)
*/
public class LongLockDemo {
private final List<String> list = new ArrayList<>();
public void badProcess(String data) {
synchronized (this) {
// 模拟耗时操作(不应该在锁内执行)
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(data);
}
}
/**
* 优化:只在必要的地方加锁
*/
public void goodProcess(String data) {
// 耗时操作放在锁外
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 只在临界区加锁
synchronized (this) {
list.add(data);
}
}
}
6.3 第三板斧:使用无锁数据结构
原则:能用CAS就不用锁
JDK自带高性能无锁工具,性能远超synchronized:
- 替代HashMap →
ConcurrentHashMap - 替代ArrayList →
CopyOnWriteArrayList - 原子计数 →
AtomicInteger/LongAdder
java
/**
* 使用AtomicInteger替代synchronized
*/
public class AtomicDemo {
// 错误:使用synchronized
private int count1 = 0;
public synchronized void increment1() {
count1++;
}
// 正确:使用AtomicInteger(无锁)
private AtomicInteger count2 = new AtomicInteger(0);
public void increment2() {
count2.incrementAndGet();
}
}
七、实战演练:完整排查流程
7.1 模拟死锁并排查
java
/**
* 实战:模拟死锁,并完整排查
*/
public class DeadlockExercise {
private static final Object LOCK1 = new Object();
private static final Object LOCK2 = new Object();
public static void main(String[] args) {
System.out.println("进程PID: " + ManagementFactory.getRuntimeMXBean().getName());
// 线程1
new Thread(() -> {
synchronized (LOCK1) {
System.out.println("线程1: 获得锁1");
sleep(100);
System.out.println("线程1: 等待锁2");
synchronized (LOCK2) {
System.out.println("线程1: 获得锁2");
}
}
}, "Thread-1").start();
// 线程2
new Thread(() -> {
synchronized (LOCK2) {
System.out.println("线程2: 获得锁2");
sleep(100);
System.out.println("线程2: 等待锁1");
synchronized (LOCK1) {
System.out.println("线程2: 获得锁1");
}
}
}, "Thread-2").start();
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) {}
}
}
7.2 完整排查步骤
步骤1:运行程序,获取PID
进程PID: 12345@localhost
步骤2:使用jstack排查
bash
jstack 12345 | grep -A 20 "deadlock"
输出:
Found one Java-level deadlock:
=============================
"Thread-2":
waiting to lock monitor 0x00007f8c5c00a500 (object 0x00000000d5f8a7f0, a java.lang.Object),
which is held by "Thread-1"
"Thread-1":
waiting to lock monitor 0x00007f8c5c00a780 (object 0x00000000d5f8a7e0, a java.lang.Object),
which is held by "Thread-2"
Java stack information for the threads listed above:
===================================================
"Thread-2":
at com.example.DeadlockExercise.lambda$main$1(DeadlockExercise.java:28)
- waiting to lock <0x00000000d5f8a7f0> (a java.lang.Object)
- locked <0x00000000d5f8a7e0> (a java.lang.Object)
"Thread-1":
at com.example.DeadlockExercise.lambda$main$0(DeadlockExercise.java:16)
- waiting to lock <0x00000000d5f8a7e0> (a java.lang.Object)
- locked <0x00000000d5f8a7f0> (a java.lang.Object)
步骤3:分析结果
- 线程1持有锁1(
0x00000000d5f8a7e0),等待锁2(0x00000000d5f8a7f0) - 线程2持有锁2,等待锁1
- 死锁位置:
DeadlockExercise.java:16和DeadlockExercise.java:28
步骤4:修复代码
java
// 修复:固定顺序获取锁
// 先锁LOCK1,再锁LOCK2,两个线程都用这个顺序

八、面试高频真题
Q1:死锁的四个必要条件是什么?
答案:
- 互斥条件:资源不能被共享
- 请求与保持:持有资源的同时请求其他资源
- 不可剥夺:已获得的资源不能被强行剥夺
- 循环等待:线程之间形成循环等待链
Q2:如何用jstack排查死锁?
答案:
jps找到进程PIDjstack <pid>导出线程栈- 搜索
deadlock关键字,JVM会自动检测并输出死锁信息 - 分析
waiting to lock和locked,定位到具体代码行
Q3:CPU 100%如何排查?
答案:
top找到高CPU进程PIDtop -Hp <pid>找到高CPU线程TIDprintf "%x\n" <tid>转十六进制jstack <pid> | grep -A 20 <hex>定位到代码行- 分析代码,找到死循环或频繁自旋的位置
Q4:Arthas的thread -b命令有什么作用?
答案:
thread -b:检测并显示死锁线程- 直接输出死锁信息,包括锁ID、持有线程、等待线程、堆栈位置
- 比jstack更简洁,针对死锁做了优化
Q5:如何预防死锁?
答案:
- 固定顺序获取锁:所有线程按相同顺序获取锁
- 定时尝试锁 :使用
tryLock,获取不到就释放已持有的锁 - 减少锁粒度:用细粒度锁降低锁竞争
- 使用无锁数据结构 :
AtomicInteger、ConcurrentHashMap
Q6:读写锁(ReadWriteLock)为什么能提升性能?
答案:
- 读锁共享,写锁互斥
- 多个读线程可以同时持有锁,提高并发度
- 适用于读多写少的场景
- 性能比
synchronized提升N倍
Q7:线上OOM如何排查?
答案:
jmap -heap <pid>查看堆内存概览jmap -dump:live,format=b,file=heap.hprof <pid>导出堆内存快照- 用MAT或VisualVM分析,找
Leak Suspects - 定位到占用内存最多的对象和代码位置
- 分析是否内存泄漏(如ThreadLocal未清理)或内存溢出(如集合无限增长)
总结
1. 核心知识点速记口诀
死锁四要素,缺一不可破,
互斥持等环,循环等待祸。
jstack来排查,找BLOCKED线程,
锁ID堆栈行,一眼定乾坤。
CPU飙上天,top先找P,
top -Hp找T,十六进制转,
jstack定位线,代码行现原形。
性能调优三板斧:
粒度要细、持锁要短、无锁优先。
2. 核心工具链总结
| 工具 | 用途 | 常用命令 |
|---|---|---|
jps |
查看Java进程 | jps -l |
jstack |
线程栈分析 | jstack <pid> |
jmap |
堆内存分析 | jmap -heap <pid> |
jstat |
GC监控 | jstat -gcutil <pid> 1000 |
Arthas |
在线诊断 | thread -b, dashboard |
top |
系统资源 | top -Hp <pid> |
MAT |
内存分析 | 分析heap.hprof |

3. 核心要点回顾
- 死锁 :四要素缺一不可,固定锁顺序是最实用预防方案;
- 排查工具:jstack(基础)、jconsole(可视化)、Arthas(生产首选);
- CPU排查:进程→线程→16进制→线程栈,Arthas可简化为1条命令;
- 性能调优:减少锁粒度、缩短锁时间、优先使用无锁;
- 核心原则:死锁重在预防,性能重在少锁。
4. 实战建议
- 生产环境 :在启动脚本中加入
-XX:+HeapDumpOnOutOfMemoryError,OOM时自动dump堆内存 - 监控告警:配置JVM监控,线程数超限、GC频繁时告警
- 代码规范 :避免在锁内执行耗时操作,使用
tryLock代替synchronized - 定期巡检:使用Arthas定期检查线上线程状态
写在最后
从死锁排查到CPU飙升定位,从jstack到Arthas,Java并发问题的诊断是一套完整的工具链 + 方法论。很多开发者遇到线上故障只会重启,殊不知重启只是"治标不治本",真正的问题还在那里。
掌握这些排查工具,不仅能让你在故障发生时快速定位 ,更能让你在代码层面 写出更健壮、更高效的并发程序。记住:好的代码不是没有bug,而是bug出现时能快速定位和修复。
如果觉得有帮助,欢迎点赞、收藏、转发!