本文目录导读:
我来给你整理一份详细的死锁排查实战案例,从现象到解决手把手拆解。
死锁场景复现
典型业务场景
一个转账系统中,同时进行A→B和B→A的转账操作,就容易触发死锁。
public class TransferService {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
// 转账方法:from向to转账
public static void transfer(String from, String to, int amount) {
// 获取第一个账户的锁
synchronized (getLock(from)) {
System.out.println(Thread.currentThread().getName()
+ " 获取到 " + from + " 的锁");
// 模拟业务处理,增加死锁概率
try { Thread.sleep(100); } catch (InterruptedException e) {}
// 获取第二个账户的锁
synchronized (getLock(to)) {
System.out.println(Thread.currentThread().getName()
+ " 获取到 " + to + " 的锁");
// 执行业务逻辑
System.out.println(from + " 向 " + to + " 转账 " + amount);
}
}
}
private static Object getLock(String account) {
return "A".equals(account) ? lockA : lockB;
}
public static void main(String[] args) {
// 线程1:A→B转账
new Thread(() -> transfer("A", "B", 100), "转账线程-1").start();
// 线程2:B→A转账
new Thread(() -> transfer("B", "A", 200), "转账线程-2").start();
}
}
死锁发生过程分析
执行流程时序
线程-1 线程-2
| |
|--获取lockA(成功) |--获取lockB(成功)
|--sleep(100ms) |--sleep(100ms)
| |
|--尝试获取lockB(等待...) |--尝试获取lockA(等待...)
| ↑ | ↑
| └── 死锁!互相等待对方释放资源
死锁产生的四个必要条件
| 条件 | 状态 | 说明 |
|---|---|---|
| 互斥条件 | ✅ 满足 | 锁只能被一个线程持有 |
| 持有并等待 | ✅ 满足 | 持有自己的锁,还等待对方的锁 |
| 不可剥夺 | ✅ 满足 | 锁只能由持有者自己释放 |
| 循环等待 | ✅ 满足 | 线程1等线程2的锁,线程2等线程1的锁 |
死锁排查实战步骤
Step 1:找到问题进程
# 方法1:使用jps查找Java进程 $ jps -l 12345 com.example.TransferService # 方法2:使用ps查找 $ ps -ef | grep java
Step 2:使用jstack分析线程栈
# 导出线程栈信息 $ jstack 12345 > thread_dump.txt # 或者直接查看 $ jstack 12345
关键输出分析:
Found one Java-level deadlock:
=============================
"转账线程-2":
waiting to lock monitor 0x0000000012345678 (object 0x00000000abcdef01,
a java.lang.Object),
which is held by "转账线程-1"
"转账线程-1":
waiting to lock monitor 0x0000000012345679 (object 0x00000000abcdef02,
a java.lang.Object),
which is held by "转账线程-2"
Java stack information for the threads listed above:
===================================================
"转账线程-2":
at com.example.TransferService.transfer(TransferService.java:24)
- waiting to lock <0x00000000abcdef01> (a java.lang.Object)
at com.example.TransferService.lambda$main$1(TransferService.java:43)
at com.example.TransferService$$Lambda$1/0x00000008.run(...)
"转账线程-1":
at com.example.TransferService.transfer(TransferService.java:24)
- waiting to lock <0x00000000abcdef02> (a java.lang.Object)
- locked <0x00000000abcdef01> (a java.lang.Object)
at com.example.TransferService.lambda$main$0(TransferService.java:39)
at com.example.TransferService$$Lambda$0/0x00000008.run(...)
Step 3:使用jconsole图形化工具排查
# 启动jconsole $ jconsole 12345
操作路径: 线程 → 检测死锁
显示效果:
线程 "转账线程-1" 正在等待 "转账线程-2" 持有的锁
线程 "转账线程-2" 正在等待 "转账线程-1" 持有的锁
Step 4:使用VisualVM深入分析
# 启动VisualVM $ visualvm
排查步骤:
- 在左侧选择目标进程
- 点击"线程"标签页
- 点击"线程Dump"按钮
线程Dump结果:
线程名称: 转账线程-1
状态: BLOCKED on ObjectMonitor
等待获取: 0x00000000abcdef02
已持有: 0x00000000abcdef01
堆栈:
- TransferService.transfer(TransferService.java:24)
- TransferService$$Lambda$0.run(...)
- java.lang.Thread.run(Thread.java:748)
线程名称: 转账线程-2
状态: BLOCKED on ObjectMonitor
等待获取: 0x00000000abcdef01
已持有: 0x00000000abcdef02
堆栈:
- TransferService.transfer(TransferService.java:24)
- TransferService$$Lambda$1.run(...)
- java.lang.Thread.run(Thread.java:748)
解决方案
方案1:固定锁顺序(推荐)
public static void transfer(String from, String to, int amount) {
// 根据账户名排序,保证锁获取顺序一致
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
Object firstLock = fromHash < toHash ? getLock(from) : getLock(to);
Object secondLock = fromHash < toHash ? getLock(to) : getLock(from);
synchronized (firstLock) {
synchronized (secondLock) {
// 业务逻辑
}
}
}
方案2:使用显式锁超时
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class TransferService {
private static final ReentrantLock lockA = new ReentrantLock();
private static final ReentrantLock lockB = new ReentrantLock();
public static boolean transfer(String from, String to, int amount) {
ReentrantLock firstLock = getLock(from);
ReentrantLock secondLock = getLock(to);
try {
// 尝试获取第一个锁,最多等待5秒
if (!firstLock.tryLock(5, TimeUnit.SECONDS)) {
return false;
}
try {
// 尝试获取第二个锁,同样设置超时
if (!secondLock.tryLock(5, TimeUnit.SECONDS)) {
return false; // 获取失败,上层决定是否重试
}
try {
// 业务逻辑
return true;
} finally {
secondLock.unlock();
}
} finally {
firstLock.unlock();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
}
方案3:数据库层面的解决方案
对于数据库操作,可以使用数据库的锁机制和事务隔离级别来避免死锁。
排查要点总结
| 排查工具 | 使用场景 | 关键信息 |
|---|---|---|
| jps | 查看进程 | 进程ID |
| jstack | 获取线程快照 | 锁的持有和等待关系 |
| jconsole | 实时监控 | 图形化死锁检测 |
| VisualVM | 深入分析 | 线程状态、锁详情 |
| jcmd | 轻量级命令 | Thread.print指令 |
| Arthas | 在线调试 | thread -n命令查看阻塞线程 |
排查口诀
一找进程 二抓快照 三看锁关系 四定位代码 五修复验证
死锁排查的核心是定位"谁持有谁的锁"以及"谁在等待谁",jstack是排查死锁最直接有效的工具。