Java死锁是线程互相等待锁而阻塞,并非直接崩溃,需通过jstack、JConsole等工具主动检测,结合统一锁序、tryLock超时、缩小同步范围及使用并发工具来修复和预防。

Java死锁不是“遇到就崩”,而是线程互相卡住、谁也不让步——关键在提前发现、快速定位、精准修复。下面直接说实战中管用的办法。
怎么快速发现是不是死锁
别等线上报警才想起查死锁。开发和测试阶段就要主动探查:
- 用 jstack + 进程ID:运行
jstack,搜deadlock或看线程状态是否大量为BLOCKED且锁等待链成环; - 用 JConsole 或 VisualVM:连上 JVM,点“检测死锁”按钮,它会自动标出涉及的线程和锁对象;
- 加 JVM 参数
-XX:+PrintConcurrentLocks(配合jstack)或-XX:+UnlockDiagnosticVMOptions -XX:+LogVMOutput(JDK9+ 更细粒度); - 业务关键路径里,对高争用资源加轻量级“锁持有超时日志”,比如记录某线程持锁超过500ms就打warn,常能暴露隐患。
怎么看懂 jstack 输出里的死锁线索
重点盯三块内容,不用全读:
-
线程名和状态:比如
"Thread-1" #12 prio=5 os_prio=0 tid=0x00007f... nid=0x1a46 waiting for monitor entry—— 表示它想进 synchronized 块但进不去; - locked :说明它当前持有一个对象锁;
- waiting to lock :它正等着另一个锁——如果另一个线程反过来也 waiting to lock 它持有的那个锁,就闭环了。
典型死锁片段长这样:
立即学习“Java免费学习笔记(深入)”;
"Thread-B" ... locked ... waiting to lock
修复死锁的 4 种靠谱做法
不靠猜,靠约束和设计:
-
统一锁顺序:多个锁必须按固定顺序获取。比如所有代码都先 lock A 再 lock B,绝不反着来。可用锁对象的
System.identityHashCode()做排序依据; -
用 tryLock() 替代 synchronized:给锁加超时,拿不到就释放已持锁并重试或降级。注意要配合
finally解锁; - 缩小同步范围:把大段逻辑拆开,只锁真正共享修改的部分。例如“查库存→扣减→写日志”,只在扣减那一步加锁;
-
用并发工具替代手写锁:优先选
ConcurrentHashMap、AtomicInteger、StampedLock或ReentrantLock的条件队列,它们内部已规避常见死锁模式。
预防比修复更重要
上线前做两件事能拦下 80% 死锁:
- 代码评审时,专门问:“这里加了几个锁?顺序确定吗?有没有嵌套调用可能引入新锁?”;
- 单元测试跑并发场景:用
CountDownLatch同时启多个线程,模拟抢同一组资源,观察是否 hang 住; - 在 CI 流程里加
jstack自动快照检查:启动应用后 sleep 2s,执行 jstack 并 grep “deadlock”,有就失败。
基本上就这些。死锁不复杂,但容易忽略锁的隐式传递和调用链变化。盯住顺序、超时、范围、工具这四点,99% 的问题都能压在上线前。










