死锁本质是多线程对多对象加锁顺序不一致导致的循环等待,需通过依赖图显性化“谁持有什么锁、正在等什么锁”,并依环定位根因:统一锁序、拆分嵌套调用或解耦JVM与DB锁。

复杂业务场景下的死锁,本质是多个线程在操作多个协作对象(如 User、Order、Payment)时,因锁获取顺序不一致,形成循环等待链。画依赖图不是为了“美术表达”,而是把隐含的锁依赖关系显性化、结构化,从而快速定位循环点。关键在于抓准“谁持有什么锁、正在等什么锁”这两个动作。
依赖图的核心要素:节点与有向边
一张有效的死锁依赖图只包含两类元素:
-
节点:代表被加锁的对象实例(不是类,是具体对象),例如
user-1001、order-2056、payment-7890;也可用锁对象本身(如lockA、this、account.lock); - 有向边 A → B:表示“当前持有 A 的线程,正在等待获取 B 的锁”。方向体现等待关系,不是调用关系。
三步画出可诊断的依赖图
以银行转账 + 用户风控联动的典型场景为例(线程1:A→B转账并校验B信用;线程2:B→A转账并校验A余额):
-
Step 1:提取每个线程的锁操作序列
从日志或 jstack 输出中确认: 线程 T1 持有account-A锁,正阻塞在account-B的synchronized块入口; 线程 T2 持有account-B锁,正阻塞在account-A的synchronized块入口。 -
Step 2:构建节点和边
画两个节点:account-A和account-B; 从account-A画箭头指向account-B(T1 的等待); 从account-B画箭头指向account-A(T2 的等待)。 -
Step 3:识别环并标记死锁
若图中存在有向环(如 A→B→A),即为死锁证据。环内所有节点对应的对象,就是参与死锁的资源;环上每条边对应一个阻塞中的线程。
处理层次化/嵌套对象的依赖图
当对象间存在双向引用(如 User 持有 Account 引用,Account 又持有 User 引用),且方法互相调用加锁时,依赖图会延伸:
分析 Java Spring Boot 后端 Controller 代码,自动生成 Markdown 格式的接口文档。当用户说“生成 Java 接口文档”、“生成 API 文档”、“从 Spring 代码生成文档”时触发。
- User-1001 → Account-5566(因
user.update()内部调用了account.validate()) - Account-5566 → User-1001(因
account.update()内部调用了user.getProfile()) - 此时图中出现长度为 2 的环,直接暴露设计缺陷:协作对象不该在同步块内反向调用对方的同步方法。
依赖图如何指导修复
图不是终点,而是修复决策的依据:
- 若环由不同锁顺序引起(如转账),强制统一顺序:按对象 hashCode 或 ID 升序加锁,消除 A→B 和 B→A 并存可能;
- 若环源于嵌套调用(如 User↔Account),拆分锁粒度:将校验逻辑移出 synchronized 块,或改用无锁数据结构/乐观锁;
- 若环涉及数据库行锁+JVM对象锁混合(如先 select for update 再 synchronized(this)),需解耦:避免 JVM 锁依赖 DB 锁的释放时机。
不复杂但容易忽略:画图时一定要基于运行时真实持有的对象实例,而不是类名或变量名;边的方向必须严格对应“持有→等待”,画反就失去诊断意义。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










