死锁的产生、检测与预防:从jstack到代码层面的解决方案
死锁的产生、检测与预防:从jstack到代码层面的解决方案
适读人群:Java后端开发者、排查过或担心死锁问题的工程师 | 阅读时长:约17分钟
开篇故事
2020年某个周五下午,线上告警:某业务系统所有接口响应超时,P99从200ms飙到了无响应。
登服务器,jstack一把,看到了这样的输出:
Found one Java-level deadlock:
=============================
"order-worker-1":
waiting to lock monitor 0x000000001234 (object of type AccountService),
which is held by "pay-worker-1"
"pay-worker-1":
waiting to lock monitor 0x000000005678 (object of type OrderService),
which is held by "order-worker-1"死锁。两个线程互相等待对方持有的锁,都进入了BLOCKED状态,系统彻底卡死。
定位到代码:OrderService.createOrder()锁了OrderService,然后调用AccountService.deduct();同时AccountService.refund()锁了AccountService,然后调用OrderService.cancelOrder()。两个方法在并发时,形成了循环等待。
这是我第一次在线上遭遇死锁。当时紧急重启服务,然后花了一个下午修复了代码。
今天把死锁的产生条件、检测方法、预防策略系统总结一下。
一、死锁的四个必要条件
死锁的产生必须同时满足以下四个条件(Coffman条件):
- 互斥(Mutual Exclusion):资源一次只能被一个线程持有
- 占有并等待(Hold and Wait):线程持有至少一个资源,并在等待获取其他资源
- 不可剥夺(No Preemption):已分配的资源不能被强制剥夺,只能被持有者主动释放
- 循环等待(Circular Wait):线程T1等T2持有的资源,T2等T3持有的资源,…,Tn等T1持有的资源
预防死锁,只需要破坏其中一个条件。
二、死锁检测与预防机制
2.1 jstack检测死锁
# 获取进程PID
jps -l
# 打印线程快照,-l显示锁信息
jstack -l <PID>
# 关键词:Found one Java-level deadlock
# 找到死锁线程后,看它们在等待哪些锁jstack输出的解读:
waiting to lock monitor 0x...:该线程在等待哪个对象的锁which is held by:该锁被哪个线程持有- 循环出现就是死锁
2.2 ThreadMXBean代码级检测
JMX提供了编程式的死锁检测:
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads);
for (ThreadInfo info : threadInfos) {
System.out.println("死锁线程:" + info.getThreadName());
System.out.println("等待锁:" + info.getLockName());
System.out.println("锁持有者:" + info.getLockOwnerName());
}
}三、完整代码实现
3.1 死锁复现与jstack诊断
package com.laozhang.concurrent.deadlock;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.concurrent.TimeUnit;
/**
* 死锁复现、检测与修复演示
*
* 场景:订单服务和账户服务互相调用,形成死锁
*
* 测试环境:JDK 11
* 演示方式:
* 1. 运行死锁版本,观察程序hang住
* 2. jstack -l <PID> 查看死锁信息
* 3. 运行修复版本,程序正常完成
*/
public class DeadlockDemo {
// 两把锁
static final Object ORDER_LOCK = new Object();
static final Object ACCOUNT_LOCK = new Object();
// ===== 有死锁的版本 =====
/**
* 创建订单:先锁ORDER,再锁ACCOUNT
*/
static void createOrder(int orderId) throws InterruptedException {
synchronized (ORDER_LOCK) {
System.out.printf("[%s] 持有ORDER锁,创建订单%d...%n",
Thread.currentThread().getName(), orderId);
Thread.sleep(100); // 模拟业务操作
synchronized (ACCOUNT_LOCK) {
System.out.printf("[%s] 持有ORDER+ACCOUNT锁,扣款...%n",
Thread.currentThread().getName());
}
}
}
/**
* 账户退款:先锁ACCOUNT,再锁ORDER(锁顺序与createOrder相反!)
*/
static void refund(int orderId) throws InterruptedException {
synchronized (ACCOUNT_LOCK) {
System.out.printf("[%s] 持有ACCOUNT锁,退款订单%d...%n",
Thread.currentThread().getName(), orderId);
Thread.sleep(100); // 模拟业务操作
synchronized (ORDER_LOCK) {
System.out.printf("[%s] 持有ACCOUNT+ORDER锁,取消订单...%n",
Thread.currentThread().getName());
}
}
}
/**
* 死锁监控线程:周期性检测是否有死锁
*/
static Thread startDeadlockDetector() {
Thread detector = new Thread(() -> {
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
while (!Thread.currentThread().isInterrupted()) {
long[] deadlocked = mxBean.findDeadlockedThreads();
if (deadlocked != null) {
System.err.println("=== 检测到死锁!===");
ThreadInfo[] infos = mxBean.getThreadInfo(deadlocked, 5);
for (ThreadInfo info : infos) {
System.err.printf("线程:%s(%s)%n",
info.getThreadName(), info.getThreadState());
System.err.printf(" 等待锁:%s(持有者:%s)%n",
info.getLockName(), info.getLockOwnerName());
}
return; // 检测到死锁后退出
}
try { Thread.sleep(500); } catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "deadlock-detector");
detector.setDaemon(true);
detector.start();
return detector;
}
public static void main(String[] args) throws InterruptedException {
startDeadlockDetector();
Thread t1 = new Thread(() -> {
try { createOrder(1); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}, "order-worker-1");
Thread t2 = new Thread(() -> {
try { refund(1); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}, "pay-worker-1");
t1.start();
t2.start();
// 等待3秒,如果还没完成,认为发生了死锁
t1.join(3000);
if (t1.isAlive()) {
System.err.println("程序可能发生了死锁!");
t1.interrupt();
t2.interrupt();
}
}
}3.2 死锁预防:固定锁顺序 + tryLock超时
package com.laozhang.concurrent.deadlock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* 死锁预防的两种方案
*
* 方案1:固定锁顺序(最简单,推荐)
* 方案2:tryLock超时(适用于锁顺序难以统一的场景)
*
* 测试环境:JDK 11
*/
public class DeadlockPrevention {
// ===== 方案1:固定锁顺序 =====
static final Object LOCK_A = new Object();
static final Object LOCK_B = new Object();
/**
* 始终按照 LOCK_A → LOCK_B 的顺序加锁
* 无论是createOrder还是refund,都遵守这个顺序
*/
static void createOrderFixed(int orderId) throws InterruptedException {
synchronized (LOCK_A) { // 始终先锁A
Thread.sleep(50);
synchronized (LOCK_B) { // 再锁B
System.out.printf("[%s] createOrder(%d)完成%n",
Thread.currentThread().getName(), orderId);
}
}
}
static void refundFixed(int orderId) throws InterruptedException {
synchronized (LOCK_A) { // 同样先锁A(固定顺序!)
Thread.sleep(50);
synchronized (LOCK_B) {
System.out.printf("[%s] refund(%d)完成%n",
Thread.currentThread().getName(), orderId);
}
}
}
// ===== 方案2:tryLock超时 =====
static final ReentrantLock RL_A = new ReentrantLock();
static final ReentrantLock RL_B = new ReentrantLock();
/**
* 使用tryLock超时:如果在指定时间内无法获取所有锁,就放弃并重试
* 适用于:锁的顺序无法统一的复杂场景
*/
static boolean tryCreateOrder(int orderId) throws InterruptedException {
boolean gotA = RL_A.tryLock(500, TimeUnit.MILLISECONDS);
if (!gotA) {
System.out.printf("[%s] 获取LOCK_A超时,放弃%n",
Thread.currentThread().getName());
return false;
}
try {
Thread.sleep(50);
boolean gotB = RL_B.tryLock(500, TimeUnit.MILLISECONDS);
if (!gotB) {
System.out.printf("[%s] 获取LOCK_B超时,放弃%n",
Thread.currentThread().getName());
return false;
}
try {
System.out.printf("[%s] tryCreateOrder(%d)完成%n",
Thread.currentThread().getName(), orderId);
return true;
} finally {
RL_B.unlock();
}
} finally {
RL_A.unlock();
}
}
static boolean tryRefund(int orderId) throws InterruptedException {
// 注意:这里加锁顺序是B→A(模拟之前会死锁的场景)
// 但因为有超时,不会死锁,只会有一方重试
boolean gotB = RL_B.tryLock(500, TimeUnit.MILLISECONDS);
if (!gotB) return false;
try {
Thread.sleep(50);
boolean gotA = RL_A.tryLock(500, TimeUnit.MILLISECONDS);
if (!gotA) return false;
try {
System.out.printf("[%s] tryRefund(%d)完成%n",
Thread.currentThread().getName(), orderId);
return true;
} finally {
RL_A.unlock();
}
} finally {
RL_B.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== 方案1:固定锁顺序 ===");
Thread t1 = new Thread(() -> {
try { createOrderFixed(1); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}, "T1");
Thread t2 = new Thread(() -> {
try { refundFixed(1); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}, "T2");
t1.start(); t2.start();
t1.join(2000); t2.join(2000);
System.out.println("方案1完成:" + !t1.isAlive() + " " + !t2.isAlive());
System.out.println("\n=== 方案2:tryLock超时 ===");
Thread t3 = new Thread(() -> {
for (int retry = 0; retry < 5; retry++) {
try {
if (tryCreateOrder(1)) return;
Thread.sleep(100); // 等待后重试
} catch (InterruptedException e) { return; }
}
System.out.println("[T3] 重试5次仍失败");
}, "T3");
Thread t4 = new Thread(() -> {
for (int retry = 0; retry < 5; retry++) {
try {
if (tryRefund(1)) return;
Thread.sleep(100);
} catch (InterruptedException e) { return; }
}
System.out.println("[T4] 重试5次仍失败");
}, "T4");
t3.start(); t4.start();
t3.join(5000); t4.join(5000);
System.out.println("方案2完成");
}
}四、踩坑实录
坑1:分布式锁的死锁问题与本地锁不同
报错现象: 微服务场景下,一个服务挂了,持有Redis分布式锁的进程没有释放锁,其他服务节点永远无法获取锁,造成"分布式死锁"。
原因分析: 本地锁在JVM退出时会被OS强制释放,但分布式锁(Redis、ZooKeeper)不会。如果获取锁的进程崩溃,锁可能永远不释放。
解法: 分布式锁必须设置过期时间(TTL),即使进程崩溃,锁也会超时释放。同时用Lua脚本或SET NX EX保证原子性。
坑2:死锁只有在特定执行时序下才出现(偶发)
报错现象: 本地测试正常,线上偶发死锁,复现困难。
原因分析: 死锁是时序相关的:只有在T1持有LOCK_A等LOCK_B的同时,T2持有LOCK_B等LOCK_A,才会死锁。如果T1在T2之前完成了所有加锁操作,就不会死锁。
本地并发量低,两个线程碰到"刚好交叉持锁"的时序概率低;线上高并发时,这个概率大幅增加。
解法: 不要依赖测试来发现死锁,要靠代码审查:检查是否存在嵌套加锁,且存在不同的加锁顺序。
坑3:嵌套synchronized导致的隐式死锁
报错现象: 代码里没有显式的嵌套synchronized,但发生了死锁。
原因分析: 隐式嵌套加锁:
// A类
synchronized void methodA() {
b.methodB(); // 调用B的同步方法(隐式嵌套:A的锁→B的锁)
}
// B类
synchronized void methodB() {
a.methodA(); // 调用A的同步方法(隐式嵌套:B的锁→A的锁)
}
// T1调用a.methodA(),持有a锁,等b锁
// T2调用b.methodB(),持有b锁,等a锁
// 死锁!解法: 审查所有synchronized方法中对外部对象方法的调用,检查是否形成循环依赖。尽量减少synchronized方法的粒度,只保护真正需要保护的代码段。
坑4:线程池中的死锁:提交任务等待其他任务
报错现象: 线程池所有线程都在等待,系统hang住,但没有ReentrantLock死锁,jstack显示的是WAITING状态。
原因分析: 线程池死锁(Task Starvation Deadlock):
ExecutorService pool = Executors.newFixedThreadPool(2);
// 任务A提交给线程池,然后等任务B的结果
pool.submit(() -> {
Future<String> futureB = pool.submit(() -> "result"); // 提交任务B
return futureB.get(); // 等待B完成(阻塞!)
});
// 如果线程池只有2个线程,两个线程都在等自己提交的子任务
// 子任务永远无法被执行(没有空闲线程)
// 死锁!解法: 避免在线程池的任务里同步等待提交给同一个线程池的子任务;或者为子任务使用独立的线程池;或者使用ForkJoinPool(它的join()可以递归工作窃取,不会死锁)。
五、总结与延伸
死锁预防的核心原则:
固定锁顺序(最有效):所有地方都按同一顺序获取多把锁。可以用对象的系统哈希值(
System.identityHashCode())决定加锁顺序,避免人工约定。缩小锁的范围:只在真正需要原子保护的代码块上加锁,减少持锁时间。
使用tryLock超时:获取锁失败时回退重试,避免永久等待。
避免在持锁时调用外部代码:外部代码可能也有锁,形成不可预期的锁顺序。
使用并发容器替代手动锁:
ConcurrentHashMap、CopyOnWriteArrayList等经过精心设计,不会产生用户级死锁。
生产监控:
- 配置JMX告警(
ThreadMXBean.findDeadlockedThreads()) - 线程池线程BLOCKED状态持续时间告警
- 定期dump jstack日志(即使没有死锁,也能帮助性能分析)
