现象
请求无限期挂起而 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 只能清除现象,相同负载下死锁会再次出现。