Java死锁是线程间循环等待锁导致的静默阻塞,表现为BLOCKED/WAITING状态、低CPU、无异常;用jstack -l可定位,破坏循环等待需统一锁获取顺序,如按identityHashCode排序,并避免嵌套锁、采用tryLock超时机制。

死锁是怎么在代码里悄悄发生的
Java死锁不是异常,不抛 Exception,也不报错,程序就卡住不动——线程状态是 BLOCKED 或 WAITING,CPU 占用低,日志没报错,但接口超时、任务停摆。本质是两个或多个线程互相持有对方需要的锁,又不释放,形成循环等待。
最典型的场景:两个线程分别按不同顺序调用两个 synchronized 方法(或进入两个 synchronized 块),而这两个方法/块各自锁了不同的对象。
- 线程 A 拿着
lockA,想进synchronized(lockB) - 线程 B 拿着
lockB,想进synchronized(lockA)
这时 JVM 线程 dump 里会明确标出 found 1 deadlock,并列出涉及的线程和锁对象。
如何用 jstack 快速定位真实死锁
别等线上炸了才查。只要进程还活着,jstack -l <pid> 是最直接的手段。注意加 -l 参数,它能打印锁的详细信息(包括 Owns Monitor 和 Waiting to Monitor Enter),否则看不到死锁链。
立即学习“Java免费学习笔记(深入)”;
- 输出里出现
Found one Java-level deadlock:就坐实了 - 关注每条线程的
java.lang.Thread.State: BLOCKED (on object monitor)行,后面跟着它在等哪个锁 - 再往上翻,找同一把锁被谁
owned(即“持有”)了
示例关键片段:
java.lang.Thread.State: BLOCKED (on object monitor) at DeadlockExample$Worker.run(DeadlockExample.java:22) - waiting to lock <0x000000071a8b9c50> (a java.lang.Object) - locked <0x000000071a8b9c60> (a java.lang.Object)
这说明该线程已锁住一个对象,正等待另一个——和另一条线程的 locked/waiting to lock 能串成环,就是死锁。
破坏循环等待:给锁定义全局顺序
死锁四条件中,“循环等待”最可控也最值得优先破坏。核心思路:所有线程获取多个锁时,必须按**同一预设顺序**申请,比如按对象哈希值排序,或按业务层级编号(如 accountLock < orderLock < paymentLock)。
- 用
System.identityHashCode(obj)比较锁对象,避免hashCode()被重写导致不稳定 - 不要用
obj.toString().hashCode()或其他业务字段,它们可能为 null 或变化 - 如果锁是类实例,可约定按类名字符串排序(如
Account.class.getName().compareTo(Order.class.getName()))
简单示例:
void transfer(Account from, Account to, BigDecimal amount) {
Object firstLock = System.identityHashCode(from) < System.identityHashCode(to) ? from : to;
Object secondLock = (firstLock == from) ? to : from;
synchronized(firstLock) {
synchronized(secondLock) {
// 执行转账
}
}
}避免嵌套锁 + 超时机制兜底
即使加了锁序,一旦逻辑变复杂(比如锁内调用外部回调、数据库查询、RPC),仍可能意外引入新锁。所以更根本的做法是:减少锁粒度、拆分同步块、不用嵌套锁。
- 绝不写
synchronized(A) { synchronized(B) { ... } }这种嵌套结构,除非你 100% 控制所有调用路径 - 用
ReentrantLock.tryLock(long, TimeUnit)替代synchronized,设置合理超时(如 3 秒),失败就回退、重试或抛业务异常 -
tryLock不会阻塞,但要注意:锁未获取成功时,必须确保资源处于一致状态(比如转账前先校验余额,失败则不扣款)
容易被忽略的一点:ReentrantLock 的 lockInterruptibly() 在线程被中断时会抛 InterruptedException,但 synchronized 不响应中断——这意味着你无法靠中断唤醒一个卡在 synchronized 里的线程。
真正难的不是写出不卡死的代码,而是当多个模块由不同人维护、锁分散在 DAO、Service、Callback 里时,没人能一眼看清锁的全貌。这时候,锁的命名、文档注释、以及 CI 中自动检查锁顺序的静态分析规则,比技巧更重要。


















