第13章 死锁诊断实战

第13章 死锁诊断实战

13.1 死锁的四个必要条件

操作系统教科书里经典的死锁四条件,在 Java 多线程场景下同样成立:互斥(锁同一时刻只能被一个线程持有)、持有并等待(线程已持有一把锁,同时在等待另一把)、不可抢占(锁不能被其他线程强制夺走)、循环等待(多个线程之间形成环形等待链) 。破坏其中任意一个条件都能避免死锁,实践中最容易落地的是破坏"循环等待"(统一加锁顺序)。

13.2 实测复现与 jstack 诊断

构造一个经典的双锁交叉死锁:

java 复制代码
static final Object lockA = new Object();
static final Object lockB = new Object();

Thread t1 = new Thread(() -> {
    synchronized (lockA) {
        Thread.sleep(200);
        synchronized (lockB) { /* ... */ }
    }
}, "Thread-1");

Thread t2 = new Thread(() -> {
    synchronized (lockB) {
        Thread.sleep(200);
        synchronized (lockA) { /* ... */ }
    }
}, "Thread-2");

进程挂起后,用 jstack <pid> 抓取,以下是 JDK 21 实测的真实输出(未做任何简化改写):

less 复制代码
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007fa9e4002ac0 (object 0x00000000c1817bb8, a java.lang.Object),
  which is held by "Thread-2"

"Thread-2":
  waiting to lock monitor 0x00007fa9f0001a60 (object 0x00000000c1817ba8, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
	at DeadlockDemo.lambda$main$0(DeadlockDemo.java:8)
	- waiting to lock <0x00000000c1817bb8> (a java.lang.Object)
	- locked <0x00000000c1817ba8> (a java.lang.Object)
	at DeadlockDemo$$Lambda/0x00007faa000009f8.run(Unknown Source)
	at java.lang.Thread.runWith(java.base@21.0.5/Thread.java:1596)
	at java.lang.Thread.run(java.base@21.0.5/Thread.java:1583)
"Thread-2":
	at DeadlockDemo.lambda$main$1(DeadlockDemo.java:14)
	- waiting to lock <0x00000000c1817ba8> (a java.lang.Object)
	- locked <0x00000000c1817bb8> (a java.lang.Object)
	at DeadlockDemo$$Lambda/0x00007faa00000c08.run(Unknown Source)
	at java.lang.Thread.runWith(java.base@21.0.5/Thread.java:1596)
	at java.lang.Thread.run(java.base@21.0.5/Thread.java:1583)

Found 1 deadlock.

JVM 会直接明确告诉你"Found one Java-level deadlock" ,并列出每个线程当前持有哪把锁(locked <地址>)、正在等待哪把锁(waiting to lock <地址>),以及具体卡在源码的哪一行(DeadlockDemo.java:8 / :14)。这是排查死锁效率最高的手段------不需要人工推理"谁等谁",JVM 已经把整个等待环直接分析出来了

排查步骤:

bash 复制代码
# 1. 找到目标 Java 进程 PID
jps -l

# 2. 抓取线程栈(推荐连续抓 2-3 次,间隔几秒,确认锁的持有关系没有变化,排除瞬时抖动)
jstack <pid> > thread_dump_1.txt
sleep 5
jstack <pid> > thread_dump_2.txt

# 3. 直接搜索关键字,无需从头逐行看
grep -A 20 "Found one Java-level deadlock" thread_dump_1.txt

# 4. 生产环境替代方案:Arthas(无需登录到目标机器执行 jstack,支持远程 attach)
thread -b   # Arthas 命令,直接列出被阻塞的线程及其阻塞原因

13.3 破坏循环等待:统一加锁顺序

java 复制代码
// 错误:t1 按 A→B 顺序加锁,t2 按 B→A 顺序加锁,形成循环等待
// 正确:所有线程都统一按照相同的顺序获取多把锁(比如按锁对象的 hashCode 或业务约定的固定顺序)
Object first = System.identityHashCode(lockA) < System.identityHashCode(lockB) ? lockA : lockB;
Object second = (first == lockA) ? lockB : lockA;
synchronized (first) {
    synchronized (second) {
        // 无论调用方以什么顺序传入两把锁,实际加锁顺序始终一致,从根本上消除循环等待
    }
}

13.4 tryLock 超时重试:避免死锁而非仅仅是检测死锁

java 复制代码
ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();

boolean gotA = lockA.tryLock(1, TimeUnit.SECONDS);
if (gotA) {
    try {
        boolean gotB = lockB.tryLock(1, TimeUnit.SECONDS);
        if (gotB) {
            try { /* 业务逻辑 */ } finally { lockB.unlock(); }
        } else {
            // 超时未拿到第二把锁,主动放弃已持有的第一把锁,避免无限期等待形成死锁
        }
    } finally { lockA.unlock(); }
}

tryLock(timeout) 从机制上破坏了"不可抢占"这一必要条件的等价效果------虽然锁本身依然不能被强制抢占,但线程可以主动选择"等太久就放弃已持有的资源",从而打破可能形成的循环等待。这是生产环境里比"统一加锁顺序"更具弹性的方案,尤其适用于加锁顺序难以在设计阶段就完全枚举清楚的复杂系统。


相关推荐
笃行3503 小时前
向量数据库单独建一套,这笔账划不划算
后端
weixin_431600443 小时前
Agent Workflow 学习向:Code 节点,比模板更强的变量加工
后端·python·学习·ai·
Rain的Java大神之路4 小时前
短信接口被狂刷怎么处理
java·运维·后端·web安全·面试·架构·xss
Lost of 程序猿4 小时前
AOP 实战:面向切面编程从理论到落地
后端·asp.net·aop·面向切片变成
Lost of 程序猿5 小时前
ASP.NET Core 认证授权实战:从 JWT 到 Policy,设计一套企业级 RBAC 权限系统
后端·asp.net
Lost of 程序猿5 小时前
ASP.NET Core 后台任务全景:从 BackgroundService 到 Channel 队列,再到分布式调度
后端·asp.net·.netcore
倾颜5 小时前
NestJS 核心概念梳理:从 Module、Controller 到 Guard、Interceptor
后端·node.js·nestjs
东风破_6 小时前
JWT 1:从一个登录请求开始,理解 React 项目里的 API 层与 Mock
前端·后端
东风破_6 小时前
JWT 3:为什么 Token 要放进 Authorization?Axios 拦截器到底解决了什么?
前端·后端
东风破_6 小时前
JWT 5:路由守卫是什么?把整个 JWT 登录鉴权流程串起来
前端·后端