news 2026/5/22 21:00:42

当你的线程“互相等待”时:死锁的四个必要条件与 Java 代码中的“致命拥抱”

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
当你的线程“互相等待”时:死锁的四个必要条件与 Java 代码中的“致命拥抱”

你打开jstack,发现几个线程都停在BLOCKED状态,谁也动不了。
日志里最后一行是“正在获取锁 A”,下一行“正在等待锁 B”……永远等不到。
恭喜,你遇到了并发编程的经典噩梦——死锁
这不是随机故障,而是四个条件恰好同时满足的必然结果。
学会识别它们,你就能从“死锁受害者”变成“死锁预防者”。

大家好,我是Evan,一个在知识汇秒杀系统中用tryLock避免过死锁的 Java+AI 学生。
今天,我们从操作系统的死锁四个必要条件出发,用真实的 Java 代码还原死锁现场,再用jstack亲手抓出它。
读完这篇,你不仅能背出“互斥、持有并等待、不可剥夺、循环等待”,还能在自己的代码里提前嗅到死锁的味道。

📌 写在前面

大二学操作系统,老师讲死锁时,我背了四个条件,但总觉得那是银行家算法里的抽象概念。
直到我在知识汇的优惠券秒杀中,用两个synchronized嵌套更新库存和用户额度,压测时线程池居然全部卡死。
jstack一拉,清清楚楚看到 Thread-1 持有锁 A 等待锁 B,Thread-2 持有锁 B 等待锁 A。
那一刻我才明白:死锁不是教科书上的古董,它就藏在你每天写的synchronized

一、死锁的四个必要条件(一个都不能少)

死锁的发生必须同时满足以下四个条件:

只有这四个条件同时成立,死锁才会发生。因此,打破任意一个,就能预防或解除死锁。

二、用 Java 代码还原一个经典死锁

public class DeadlockDemo { private static final Object LOCK_A = new Object(); private static final Object LOCK_B = new Object(); public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (LOCK_A) { System.out.println("Thread-1 持有 LOCK_A,等待 LOCK_B"); sleep(100); // 模拟业务处理 synchronized (LOCK_B) { System.out.println("Thread-1 同时持有 LOCK_A 和 LOCK_B"); } } }); Thread t2 = new Thread(() -> { synchronized (LOCK_B) { System.out.println("Thread-2 持有 LOCK_B,等待 LOCK_A"); sleep(100); synchronized (LOCK_A) { System.out.println("Thread-2 同时持有 LOCK_B 和 LOCK_A"); } } }); t1.start(); t2.start(); } private static void sleep(int ms) { try { Thread.sleep(ms); } catch (InterruptedException e) {} } }

运行结果(大概率):

Thread-1 持有 LOCK_A,等待 LOCK_B Thread-2 持有 LOCK_B,等待 LOCK_A (然后程序卡死,永不退出)

四个条件检查

  • 互斥:synchronized保证互斥 ✅

  • 持有并等待:t1 持有 A 等 B,t2 持有 B 等 A ✅

  • 不可剥夺:t1 不会释放 A 去等 B(除非主动 unlock) ✅

  • 循环等待:t1 等 t2 释放 B,t2 等 t1 释放 A ✅

完美死锁。

三、如何检测死锁:jstack是你的火眼金睛

3.1 找到 Java 进程 PID

jps -l

3.2 用jstack打印线程堆栈

jstack <pid>

输出中会明确提示Found one Java-level deadlock

Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x00007f8c1400a500 (object 0x000000076b5a3b20, a java.lang.Object), which is held by "Thread-2" "Thread-2": waiting to lock monitor 0x00007f8c1400a2e0 (object 0x000000076b5a3b10, a java.lang.Object), which is held by "Thread-1"

还会告诉你哪一行代码导致的。

3.3 其他工具

  • VisualVM:图形化监控,可自动检测死锁。

  • JConsole:连接进程 → 线程 → 检测死锁按钮。

  • Linuxkill -3 <pid>:打印堆栈到标准错误(老式方法)。

四、开发中常见的死锁场景

4.1 嵌套synchronized

如上面的例子,两个锁顺序相反。

4.2 数据库行锁 + 表锁

-- 事务1 SELECT * FROM orders WHERE id = 1 FOR UPDATE; -- 锁住行1 SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 等待锁住行2 -- 事务2 SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 锁住行2 SELECT * FROM orders WHERE id = 1 FOR UPDATE; -- 等待锁住行1

数据库也会死锁,通常数据库会检测并回滚其中一个事务(返回Deadlock found when trying to get lock)。

4.3 线程池 + Future.get() 相互等待

ExecutorService pool = Executors.newFixedThreadPool(2); Future<?> f1 = pool.submit(() -> { Future<?> f2 = pool.submit(() -> {}); f2.get(); // 等待 f2 }); Future<?> f2 = pool.submit(() -> { Future<?> f1 = pool.submit(() -> {}); f1.get(); // 等待 f1 });

如果线程池只有 2 个线程,且两个任务互相提交并等待对方完成,就会死锁。

五、预防死锁的四种武器(打破任一条件)

5.1 顺序加锁(打破循环等待)

java

// 规定总是先锁 A 再锁 B synchronized (LOCK_A) { synchronized (LOCK_B) { // 安全 } }

5.2 使用tryLock超时(打破持有并等待 + 不可剥夺)

ReentrantLock lockA = new ReentrantLock(); ReentrantLock lockB = new ReentrantLock(); boolean gotA = lockA.tryLock(100, TimeUnit.MILLISECONDS); boolean gotB = lockB.tryLock(100, TimeUnit.MILLISECONDS); if (gotA && gotB) { try { // 执行业务 } finally { if (gotB) lockB.unlock(); if (gotA) lockA.unlock(); } } else { // 释放已获得的锁,避免死锁 if (gotA) lockA.unlock(); if (gotB) lockB.unlock(); // 重试或降级 }

5.3 一次性申请所有资源(打破“持有并等待”)

// 使用 All-or-Nothing 模式 ReentrantLock[] locks = {lockA, lockB}; while (true) { boolean acquired = true; for (ReentrantLock lock : locks) { if (!lock.tryLock(100, TimeUnit.MILLISECONDS)) { acquired = false; // 释放已获得的锁 for (ReentrantLock l : locks) l.unlock(); break; } } if (acquired) { try { /* 业务 */ } finally { for (ReentrantLock l : locks) l.unlock(); } break; } // 短暂等待后重试 }

六、数据库死锁的特殊处理

数据库死锁时,InnoDB 会检测并自动回滚其中一个事务(通常是开销较小的一方)。
你可以通过SHOW ENGINE INNODB STATUS查看最近死锁信息。

Java 中捕获

try { // 执行 SQL } catch (DeadlockLoserDataAccessException e) { // 重试 }

预防

  • 统一对表的访问顺序(例如先更新主表,再更新从表)。

  • 使用SELECT ... FOR UPDATE NOWAIT(部分数据库支持)。

📝 总结

核心结论

  • 死锁是并发编程中“四个条件同时满足”的必然结果。

  • jstack是检测 Java 死锁的第一利器。

  • 预防死锁最实用的两个方法:固定加锁顺序+使用tryLock超时

  • 数据库死锁不同,引擎会自动回滚,但你的代码仍需要重试机制。

🤔思考题
你有一个方法,需要同时获取三个锁:LOCK_ALOCK_BLOCK_C。你规定所有线程都按 A → B → C 的顺序获取。
但是,某些情况下,你调用的第三方库内部会反向获取LOCK_CLOCK_B。你的代码无法修改第三方库。
问题:这种情况下,如何避免死锁?请给出至少两种方案。

欢迎在评论区留下你的想法 —— 下一篇我会聊聊“I/O 多路复用与 Agent 循环:epoll 如何支撑你上千个并发 Tool 调用”

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/22 20:59:45

瑞萨RA8 MCU开发入门:基于e2 studio与FSP创建基础工程全流程

1. 项目概述与核心价值最近在捣鼓瑞萨电子的RA8系列MCU&#xff0c;这颗基于Arm Cortex-M85内核的芯片性能确实猛&#xff0c;主频高达480MHz&#xff0c;还集成了Helium™技术&#xff08;MVE&#xff09;&#xff0c;在边缘AI和复杂控制场景下潜力巨大。但好东西上手总得有个…

作者头像 李华
网站建设 2026/5/22 20:55:44

对比直接使用原厂api体验taotoken在成本控制上的优势

&#x1f680; 告别海外账号与网络限制&#xff01;稳定直连全球优质大模型&#xff0c;限时半价接入中。 &#x1f449; 点击领取海量免费额度 对比直接使用原厂 API 体验 Taotoken 在成本控制上的优势 在模型应用开发过程中&#xff0c;成本是开发者必须关注的核心要素之一。…

作者头像 李华
网站建设 2026/5/22 20:52:32

拒绝玩具CRUD:用 5 款全栈离线“仓储管理”微系统精通前后端解耦(附专家级级联 Prompt)

各位全栈同仁、大前端极客以及正在突破技术瓶颈的开发者们&#xff0c;大家好。作为一名每天和分布式架构、数据库事务以及前端复杂状态流打交道的工程师&#xff0c;今天想和大家聊聊全栈工程落地中的“咬合力”。在很多技术社区里&#xff0c;大家往往能看到各种速成的单表 C…

作者头像 李华
网站建设 2026/5/22 20:47:43

如何用Python脚本实现大麦网自动化抢票?终极抢票指南

如何用Python脚本实现大麦网自动化抢票&#xff1f;终极抢票指南 【免费下载链接】Automatic_ticket_purchase 大麦网抢票脚本 项目地址: https://gitcode.com/GitHub_Trending/au/Automatic_ticket_purchase 还在为抢不到热门演唱会门票而烦恼吗&#xff1f;每次开票瞬…

作者头像 李华
网站建设 2026/5/22 20:46:54

写给前端的 CANN-ops-rand:昇腾随机数生成算子库到底是啥?

之前做强化学习&#xff0c;兄弟问我&#xff1a;“哥&#xff0c;我想在昇腾上做蒙特卡洛模拟&#xff0c;随机数生成有现成的库吗&#xff1f;” 好问题。今天一次说清楚。 ops-rand 是啥&#xff1f; ops-rand Operations for Random&#xff0c;昇腾随机数生成算子库。 一…

作者头像 李华
网站建设 2026/5/22 20:44:24

Karpathy投奔Anthropic:一个顶级AI天才的四次人生豪赌

5月19日&#xff0c;一条推文炸了整个AI圈。 Andrej Karpathy——OpenAI联合创始人、前特斯拉AI总监、AI教育布道师——宣布加入Anthropic。 英伟达具身智能负责人Jim Fan评论说&#xff1a;"这比Google I/O的Keynote更重磅。" 网友打了个比方&#xff1a;"堪…

作者头像 李华