死锁,就是两个或以上的线程各自占着对方想要的锁、又互相等待对方先放手,结果谁都走不下去,程序卡死不动。你写多线程程序时,最怕的就是本地跑得好好的,一上量就偶尔卡住,日志停在某一行再也不动,重启又能跑——这种「薛定谔的卡死」十有八九是死锁。本文基于 Java / Python 多线程实测,讲清死锁到底是什么、它的四个必要条件、一个最小复现例子、怎么避免,以及卡住了怎么排查。看完你不仅能认出死锁,还能在写代码时把它挡在门外。先给个定心丸:死锁虽然吓人,但它是「可以预防、可以排查」的,不是玄学。今天这篇文章,编程狮就把这块讲透。
一、先搞清楚:死锁到底是什么
在 Java 教程 的多线程章节里你会学到,锁是为了保护共享资源、防止多个线程同时改写导致数据错乱。但锁用不好,就会反噬自己。
死锁的本质,是循环等待:A 线程拿着锁 1、等锁 2;B 线程拿着锁 2、等锁 1,两人互相等,谁都释放不了。计算机科学给死锁总结了四个必要条件,四个同时成立才会发生:
- 互斥:资源同一时刻只能被一个线程占用;
- 占有且等待:线程拿着一个锁,还去等另一个锁;
- 不可抢占:锁不能被强制从持有者手里抢走;
- 循环等待:多个线程形成「你等我、我等你」的环。
只要打破其中任意一条,死锁就建不起来。后面讲避免,就是在讲怎么拆这四条。打个比方:两家餐厅各只有一把刀和一把叉,A 桌拿走了刀等叉、B 桌拿走了叉等刀,谁都没法吃饭——这就是现实里的循环等待。理解了这个画面,后面所有「避免办法」都是在想办法不让这种僵局出现。
二、一个最小复现例子
下面用 Java 写一段最经典的死锁:两个线程、两把锁,但加锁顺序相反。
Object lockA = new Object();
Object lockB = new Object();
// 线程 1:先锁 A,再锁 B
new Thread(() -> {
synchronized (lockA) {
synchronized (lockB) { /* 干活 */ }
}
}).start();
// 线程 2:先锁 B,再锁 A
new Thread(() -> {
synchronized (lockB) {
synchronized (lockA) { /* 干活 */ }
}
}).start();
上面这段里,线程 1 拿到 lockA 后想拿 lockB,可 lockB 正被线程 2 占着;线程 2 拿到 lockB 后想拿 lockA,又被线程 1 占着——两人僵住,程序卡死。注意死锁之所以发生,根子在于「两个线程加锁顺序相反」:只要顺序一致,环就断不了。关于多线程怎么搭起来,Java 多线程写法 讲得更细,可以对照看。
三、怎么避免死锁
既然死锁要四个条件同时成立,避免的思路就是打破其中一条,最常用的是「破坏循环等待」:
- 固定加锁顺序:所有线程都按同一顺序申请锁(永远先 A 后 B),环就断了。把上面例子里线程 2 也改成先锁 A,死锁立刻消失。
- 一次性申请所有资源:进临界区前把需要的锁全拿到,拿不全就全不放,避免「占着等」。
- 加超时:用
tryLock(timeout),等不到就放弃并回退,给系统一个「松手」的机会。 - 缩小锁范围:只在真正改共享数据时才加锁,锁住的时间越短,撞车概率越低。
在 Python3 教程 里用 threading 模块时同理:多个 Lock 也要约定统一获取顺序,否则 acquire 互相等待一样会卡死。把「统一顺序」落成代码,最直观的就是给锁编个号,所有地方都按编号从小到大申请:
// 约定永远先申请编号小的锁
synchronized (lockA) { // lockA 编号小,先拿
synchronized (lockB) { // lockB 编号大,后拿
/* 干活 */
}
}
只要全项目都遵守这条规则,循环等待就无从形成,死锁在架构层面就被掐灭了。
四、卡住了怎么排查
真遇上死锁,别靠猜,按步骤来:
- 复现并抓线程栈:Java 用
jstack <pid>导出线程 dump;Python 可用faulthandler打印栈。 - 找「BLOCKED / waiting to lock」:线程栈里会出现互相等待同一把锁的环,一眼就能定位。
- 看卡在哪一行:dump 会告诉你每个线程停在哪个方法、等哪把锁,对照代码就能找到加锁顺序不一致的源头。
- 加日志验证:在加锁前后打印线程名和锁名,复现时日志会停在某个锁之后不再前进。
为什么高并发场景里死锁更常见,高并发与 Java 从选型角度解释了线程密集时的风险来源,值得结合看。补充一点:生产环境抓 dump 要趁「卡住的那一刻」,程序一旦被重启,现场就没了。很多团队会配一个监控脚本,发现某线程连续几分钟处于 BLOCKED 就自动 jstack 存档,这比事后靠记忆复现靠谱得多。
五、两个常见误解
- 误解一:单线程不会有死锁。对,死锁的前提就是多个执行流互相等,单线程不存在「互等」。
- 误解二:加锁越多越安全。恰恰相反,锁越多、嵌套越深,越容易凑齐循环等待的四个条件。能不用锁就不用,能用局部变量就别碰共享。
死锁与饥饿、活锁的区别
死锁常和另外两个概念混为一谈,分清它们有助于排查。饥饿是指某个线程一直拿不到资源、永远在排队,但不一定互相等待,可能是优先级或调度不公平导致的;活锁是指线程们都在运行、都在互相礼让,却始终没人能往前推进,像两个人在走廊里不停给对方让路。只有死锁是全员卡死、完全停摆。排查时如果线程栈显示大家都在 waiting to lock 且形成环,那是死锁;如果只是某个线程长时间拿不到锁,更可能是饥饿;如果日志显示线程频繁重试又放弃,则偏向活锁。死锁、饥饿、活锁三者现象不同,药方也不同,把现象对号入座才能用对方法。
总结
死锁是多个线程互相占着对方要的锁、循环等待导致全员卡死。要点带走:它必须满足互斥、占有且等待、不可抢占、循环等待四个条件;最实用的避免办法是统一加锁顺序、加超时、缩小锁范围;排查靠线程栈找「waiting to lock」的环。写多线程时把加锁顺序想清楚,死锁就进不了门。下一步,把这套认知用到实际项目的并发控制里,你的程序会更稳。
延伸学习
想真正吃透并发,可以按这个顺序来:
- 先过一遍 Java多线程讲解,把线程、锁、同步练熟;
- 想看 Python 侧的并发实战,Python 并发编程实战 是边学边写的形式,适合巩固。
常见问题
Q:死锁和「活锁」是一回事吗?
A:不是。死锁是全员卡住不动;活锁是线程都在跑、都在让,却始终推进不了任务,像两个人不停互相礼让谁都过不去。
Q:加了 synchronized 就一定会死锁吗?
A:不会。死锁要多个锁形成循环等待才发生。只用一个锁、或所有地方按同一顺序加锁,通常不会死锁。
Q:线上程序怀疑死锁,但不敢随便重启怎么办?
A:先 jstack 抓线程栈定位到互等的锁和代码行,确认是死锁后再决定能否热修复;盲目重启会丢掉现场,下次还复现。

免费 AI IDE



