← 中文资源

简体中文版本

Java 死锁 — 读懂 jstack 并修复加锁顺序

用 jstack 抓取线程转储,读取死锁报告与锁持有者,通过全局加锁顺序或 tryLock 超时解除 Java 死锁。

阅读英文原文

现象

请求无限期挂起而 CPU 使用率很低;jstack 输出 Found one Java-level deadlock;两个线程处于 BLOCKED,互相等待对方持有的监视器;只有重启应用才恢复。

诊断

先定位进程并抓取带锁信息的线程转储:

jps -l
jstack -l 12345 > /tmp/threaddump.txt
grep -A40 'Found one Java-level deadlock' /tmp/threaddump.txt
jcmd 12345 Thread.print -l > /tmp/threaddump2.txt
top -H -p 12345

jps -l 列出 JVM 进程,选应用进程而不是构建工具。死锁报告会给出线程名、各自等待的监视器及持有者,堆栈行号就是相反顺序加锁的位置。间隔几秒抓两次,若两次都相同,说明是持续死锁而不是偶发慢调用。top -H 显示死锁线程几乎不耗 CPU,可据此与活锁或 GC 问题区分。

修复

为所有加锁路径定义全局顺序,例如先按账户 ID 排序再获取锁:

Account first = from.id() < to.id() ? from : to;
Account second = first == from ? to : from;
synchronized (first) {
    synchronized (second) {
        from.debit(amount);
        to.credit(amount);
    }
}

无法保证顺序时改用 ReentrantLock.tryLock(timeout),获取失败即返回并重试,且必须在 finally 中释放;同时缩小锁范围,持锁期间不要调用外部组件。单线程写入或并发容器可以彻底消除共享锁。

验证

重新运行并发场景并再次抓取 jstack,不应再出现死锁段落;阻塞线程数与吞吐恢复正常。把该并发路径加入压力测试,确保持锁顺序持续被覆盖。仅重启 JVM 只能清除现象,相同负载下死锁会再次出现。