直接搜索“found one java-level deadlock:”,有则确认死锁,无则基本排除jvm级死锁;需配合jstack -l分析锁依赖链,注意对象地址严格一致、区分locked与waiting to lock,并结合代码验证反向加锁逻辑。

怎么看 jstack 输出里有没有死锁
直接搜 Found one Java-level deadlock: 这行。有,就真死锁了;没这行,基本可以排除 JVM 级别的死锁(但不等于没业务逻辑卡死)。注意它只检测 synchronized 和 java.util.concurrent 中部分显式锁(如 ReentrantLock)的循环等待,StampedLock 或自定义锁机制不会被识别。
常见误判点:
- 看到一堆线程停在
WAITING或BLOCKED就以为是死锁 —— 实际可能是正常阻塞、IO 等待或单点竞争激烈 - 忽略线程状态时间戳:
jstack是快照,两次 dump 间隔几秒,如果线程状态没变,才值得深挖 - 把
parking to wait for <code> 和 <code>waiting on condition当成死锁信号 —— 它们只是线程挂起,不涉及锁持有关系
jstack -l 比默认输出多什么
-l 会额外打印 java.util.concurrent 锁的详细持有者和等待者信息,比如 ReentrantLock 的 AbstractQueuedSynchronizer$ConditionObject 队列、StampedLock 的读写状态,以及每个线程持有的 OwnableSynchronizer 实例。
没加 -l 时,死锁检测可能漏掉 ReentrantLock.lock() 场景;加了之后,即使没触发 JVM 死锁检测,也能手动比对「谁持有了哪把锁」「谁在等哪把锁」。
立即学习“Java免费学习笔记(深入)”;
实操建议:
- 排查疑似死锁必加
-l,哪怕多几屏输出 - 注意输出里重复出现的
locked <code> 和 <code>waiting to lock <code> 行 —— 它们构成锁依赖链 - 如果看到
Locked ownable synchronizers:下为空,说明该线程没持任何java.util.concurrent锁,不用往那边追
怎么从 dump 里还原锁依赖图
死锁本质是线程 A 持锁 1 等锁 2,线程 B 持锁 2 等锁 1。还原靠三类线索:java.lang.Thread.State、
locked <code>、<code>waiting to lock <code>。</p><div class="aritcle_card flexRow">
<div class="artcardd flexRow">
<a class="aritcle_card_img" href="/ai/2187" title="蛙蛙写作——超级AI智能写作助手"><img
src="https://img.php.cn/upload/ai_manual/001/246/273/68b6c6e349825299.png" alt="蛙蛙写作——超级AI智能写作助手" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a href="/ai/2187" title="蛙蛙写作——超级AI智能写作助手">蛙蛙写作——超级AI智能写作助手</a>
<p>蛙蛙写作辅助AI写文,帮助获取创意灵感,提供拆书、小说转剧本、视频生成等功能,是一款功能全面的AI智能写作工具。</p>
</div>
<a href="/ai/2187" title="蛙蛙写作——超级AI智能写作助手" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>
</div>
</div><p>一个典型片段:</p><pre class="brush:php;toolbar:false;">Thread-1:
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.Service.doWork(Service.java:42)
- waiting to lock <0x000000071a2b3c40> (a java.lang.Object)
- locked <0x000000071a2b3c58> (a java.lang.Object)
<p>Thread-2:
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.Service.doOther(Service.java:66)</p><ul><li>waiting to lock <0x000000071a2b3c58> (a java.lang.Object)</li><li>locked <0x000000071a2b3c40> (a java.lang.Object)
这里就能画出:Thread-1 → 持 0x...c58 → 等 0x...c40;Thread-2 → 持 0x...c40 → 等 0x...c58。闭环成立。
容易踩的坑:
- 对象地址(如
0x000000071a2b3c40)必须完全一致才算同一把锁,差一位都不行 - 别混淆
locked(已持有)和waiting to lock(正在抢),后者不表示“已经卡住”,只是当前尝试失败 - 如果锁对象是
String或常量池对象,地址可能复用,需结合类名和调用栈交叉验证
为什么本地复现不了,但生产 dump 显示死锁
根本原因是竞争窗口极窄:两个线程必须在毫秒级内以相反顺序获取同一组锁。本地环境负载低、调度顺、数据少,很难凑齐这个时机;生产环境并发高、GC 干扰多、网络延迟抖动,反而容易暴露。
更隐蔽的问题是锁粒度与执行路径差异:
- 开发环境走 mock 分支,跳过了某段加锁逻辑;生产走真实 DB 调用,路径变长,锁持有时间拉长
- 日志级别不同导致同步块内耗时变化(比如生产开了 DEBUG 日志,toString() 触发慢操作)
- JVM 参数差异:生产开了
-XX:+UseG1GC,GC 停顿让某个线程卡在临界区更久,放大竞争概率
所以别只盯着 dump 里的锁对象,得回代码看这两条路径是否真的存在反向加锁顺序 —— 那才是根因。很多所谓“偶发死锁”,其实是设计上就埋了循环依赖,只是平时没撞上。







