死锁典型模式是多线程以不同顺序获取同一组锁,如线程A先锁accountA再锁accountB,线程B反之;常用tryLock()超时避免、统一锁顺序预防、jstack和ThreadMXBean排查。

死锁发生的典型代码模式
Java中死锁最常出现在多个线程以不同顺序获取同一组 Object 锁(或 synchronized 块)时。比如线程 A 先锁 accountA 再尝试锁 accountB,而线程 B 正好相反——这种交叉加锁是死锁的直接诱因。
常见错误现象包括:程序卡住无响应、CPU 占用率低、线程状态长期为 BLOCKED(可通过 jstack 查看线程堆栈,若看到两个以上线程互相等待对方持有的锁,基本可确认)。
- 只在同步块内调用可能阻塞或依赖其他锁的方法,极易引入隐式锁依赖
- 使用
ReentrantLock时调用lock()而非tryLock(long, TimeUnit),失去超时控制能力 - 数据库事务 + JVM 锁混合使用时,JDBC 连接持有和对象锁顺序不一致,也会触发跨层死锁
如何用 tryLock() 主动规避死锁
ReentrantLock.tryLock(long, TimeUnit) 是最实用的防御手段——它允许你设定等待上限,失败后可释放已持锁并重试或回退,打破循环等待条件。
ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();
void transfer(Account from, Account to, BigDecimal amount) {
while (true) {
if (lockA.tryLock(100, TimeUnit.MILLISECONDS) &&
lockB.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
from.withdraw(amount);
to.deposit(amount);
return;
} finally {
lockA.unlock();
lockB.unlock();
}
} else {
// 至少一个锁没拿到,先释放已获得的锁,避免僵持
if (lockA.isHeldByCurrentThread()) lockA.unlock();
if (lockB.isHeldByCurrentThread()) lockB.unlock();
Thread.sleep(10); // 避免忙等
}
}
}
注意:tryLock() 不支持公平锁的“排队语义”,且必须严格配对 unlock(),否则会导致锁泄漏。
立即学习“Java免费学习笔记(深入)”;
统一锁顺序是成本最低的预防方式
如果所有线程都按相同规则获取锁(例如总是先锁 ID 小的对象),就能从根源上消除循环等待。这不需要额外 API,只需约定和校验。
使用场景:账户转账、资源池分配、树形结构遍历等存在天然可比较标识的场景。
- 对锁对象实现
Comparable,或提取唯一可比字段(如account.getId()) - 加锁前先排序:
List—— 用System.identityHashCode()是安全兜底,但不保证跨 JVM 一致,仅限单 JVM 内有效 - 避免在锁内做任何可能引发新锁竞争的操作(如调用外部服务、访问 synchronized 集合)
排查与监控不能只靠日志
死锁往往在压测或上线后才暴露,仅靠业务日志很难定位。必须结合 JVM 自带机制和轻量级埋点。
关键操作:
- 启动参数加入
-XX:+PrintConcurrentLocks -XX:+PrintGCDetails,配合jstack -l可输出显式锁持有关系 - 定期调用
java.lang.management.ThreadMXBean.findDeadlockedThreads()做主动探测(建议封装为健康检查端点) - 对高风险同步块添加计时日志:
long start = System.nanoTime(); ... log.warn("sync block took {}ms", (System.nanoTime()-start)/1_000_000);,持续偏高的耗时是潜在死锁前兆
真正难处理的是“锁链过长”:不是两把锁互等,而是 A→B→C→D→A 形成环路,这种靠人工 review 几乎不可控,必须依赖工具链自动分析锁获取路径。










