直接搜“Found one Java-level deadlock:”,有则确认死锁,无则基本排除JVM级死锁;需结合jstack -l输出的locked和waiting to lock地址链分析循环等待关系。

怎么看 jstack 输出里有没有死锁
直接搜 Found one Java-level deadlock 这行。jstack(OpenJDK 8+)只要检测到死锁,一定会在输出最开头或显眼位置打印这句,后面紧跟着参与死锁的线程列表。没这行,基本可以排除 JVM 层面的 Java 对象锁死锁(注意:不包括 native 层死锁或自旋锁等非 synchronized/reentrantlock 场景)。
常见干扰项:java.lang.Thread.State: BLOCKED 单独出现不等于死锁——它只是表示线程在等锁,可能是正常排队;只有多个线程形成「持有 A 等 B,持有 B 等 A」的闭环时,才算死锁。
怎么从线程栈里定位哪两把锁在打架
找到死锁段落后,重点看每个线程的两行关键信息:
-
locked <0x000000076b45a290>:当前线程已持有的锁对象地址 -
waiting to lock <0x000000076b45a2c0>:当前线程正在等待的锁对象地址
把所有涉及线程的这两类地址串起来,就能画出等待环。比如:
“Thread-0”: locked <0x000000076b45a290>, waiting to lock <0x000000076b45a2c0> “Thread-1”: locked <0x000000076b45a2c0>, waiting to lock <0x000000076b45a290>
这就构成一个长度为 2 的循环等待链。地址一致才代表是同一把锁,别被不同线程名或日志顺序带偏。
为什么加了 -l 参数还是看不出锁的对象类型
jstack -l <pid> 会显示更详细的锁信息,包括 java.util.concurrent.locks.ReentrantLock 或 synchronized 持有的对象实例,但前提是锁对象本身重写了 toString() 或是常见类型(如 String、Object)。如果锁是自定义类且没实现 toString(),你看到的仍是 locked <0x...> 这种地址形式。
此时得结合源码和线程栈里的 at 行定位:
- 找
at com.example.Service.update(Order, Inventory)这类调用行 - 对照代码,确认该方法里
synchronized(objA)和synchronized(objB)的实际变量名 - 再回看其它线程是否以相反顺序锁了这两个变量
OpenJDK 17+ 的特别注意点
OpenJDK 17 开始默认启用 ZGC 或 Shenandoah 时,jstack 输出可能略慢,且偶尔因 GC 线程暂停导致部分线程状态暂时不准。建议连续采样 2–3 次,对比是否每次都能稳定复现同一组死锁线程和锁地址。
另外,OpenJDK 17+ 的 jstack 不再支持 -F 强制 dump(已被移除),如果进程无响应,优先用 kill -3 <pid>(Linux/macOS)或 Ctrl+Break(Windows 控制台)触发 JVM 自身 dump,这个方式更可靠。
真正难的不是识别死锁,而是确认那两个锁地址对应的是哪两个业务资源——比如订单锁和库存锁,名字一样但实例不同,就容易误判。别跳过源码比对这步。


















