深蓝黑背景的品牌封面:中央大标题「多线程死锁:原因、排查与预防」,副标题「四个必要条件 · 统一锁顺序 · jstack 定位」,左上标注栏目「AI 工程面试题解专栏」,右上标注「面试 · Day 09」,背景为双环相扣装饰与青紫光晕 图 1:本文封面,主题为多线程死锁的原因、排查与预防(经典题解析)。

想象面试现场:面试官问,多线程程序为什么会死锁?线上碰见了怎么排查?写代码时又怎么预防?这道题属于经典题解析(历史题目复盘,见文末来源说明),它看起来基础,但能一层层追问下去——只背出「四个必要条件」的候选人,和能讲清排查动作、预防策略的候选人,差距立刻拉开。

第一步:把死锁现场讲清楚

答题先给画面,再给定义。经典现场是:线程 A 持有锁 1,申请锁 2;线程 B 持有锁 2,申请锁 1。两把锁各在一人手里,两个人都在等对方先放手,于是谁也无法继续——这就是死锁:两个或多个线程互相等待对方占有的资源,全部永远阻塞。Oracle 官方 Java 教程里有一个著名的演示:两个极其讲礼貌的人互相鞠躬,规则是鞠躬后必须等对方回礼才能直起身,结果两人同时鞠躬,谁也等不到对方先动。一个生活化例子,比定义更好记。

第二步:四个必要条件

结构图:左侧为死锁现场——线程 A 持有锁 1 等待锁 2,线程 B 持有锁 2 等待锁 1,构成循环等待环;右侧列出死锁四个必要条件(互斥、占有且等待、不可剥夺、循环等待),并指出预防就是破坏任意一条,最常用做法是全局统一加锁顺序;底部标注排查手段:jstack / ThreadMXBean 检测死锁环 图 2:死锁的循环等待现场、四个必要条件与排查预防手段。

死锁发生必须同时满足四条,缺一条都不成立。互斥:资源一次只能被一个线程占有。占有且等待:线程拿着至少一个资源,同时申请别人手里的资源。不可剥夺:已占有的资源不能被强行抢走,只能主动释放。循环等待:存在一条线程互相等待的环路,A 等 B、B 等 A。这四条是理论骨架,回答时必须一口气说全,因为后面的预防策略就是逐条「拆」它们。

第三步:线上怎么排查

死锁最麻烦的地方是不报错、不崩溃,只是某些线程再也不动了,表现为接口超时、任务堆积。排查的标准动作是抓线程转储:用 jstack 或 jcmd 的 Thread.print 命令打印目标进程全部线程的调用栈与锁持有情况——Oracle 官方文档对 jcmd 的说明写得很明确,Thread.print 用于打印所有线程及其堆栈。转储里会直接标出「Found one Java-level deadlock」之类的死锁环,环上的线程各持有哪把锁、在等哪把锁,一目了然。日常编码中也可以用 JVM 的管理接口周期检测死锁线程,把「事后翻日志」变成「主动告警」。如果面试允许,补一句动手经验会更有说服力:本地写一个两把锁、加锁顺序相反的小程序,跑几次复现卡住,再抓一次线程转储亲眼看到死锁环——整个排查链路就完整走通过一遍。

第四步:预防,破坏任意一条

四个条件是「且」的关系,所以预防的逻辑很优雅:拆任意一条即可。拆循环等待,也是最常用的做法:给所有锁定一个全局顺序,任何线程都按同一顺序加锁,环路在数学上就不可能形成。拆占有且等待:一次性申请全部所需资源,拿不到就全不拿。拆不可剥夺:用带超时的尝试加锁,拿不到就释放已持有的、退避重试。拆互斥通常做不到——锁存在的意义就是互斥,这条了解即可。面试里能按「四条件—对应四种拆法」的对仗结构答出来,层次感就有了。

工程应用与 AI 关联

死锁不是背来应付面试的。AI 推理服务和数据管道大多是高并发后端:批量任务并发落库、多个 worker 竞争共享资源、连接池与锁交织——锁粒度过大、加锁顺序不一致,都是线上事故的常见根因。面试官问死锁,真正想确认的是你有没有「共享状态即风险」的直觉。

两个易错点

一是把死锁和「程序慢」混为一谈:死锁是永久阻塞,不是性能差;顺带有两个邻居概念也要分清——活锁是线程没卡住但一直在做无用功(比如互相礼让、不断重试),饥饿是某些线程长期抢不到资源,三者表象相似、根因不同。二是只答条件不答动作,排查和预防才是区分度所在。答题节奏建议:半分钟讲死锁现场,一分钟背全四条件,然后主动把排查命令和「拆条件」的预防矩阵补上,最后用一句高并发服务场景收尾。

除了统一加锁顺序,你在项目里还用过哪些避免死锁的手段——超时重试、无锁结构还是单线程化?欢迎分享实战经验。

参考资料

  • Deadlock(Oracle 官方 Java 教程:死锁定义为两个或多个线程互相等待而永久阻塞,并以两人互相鞠躬的示例程序演示死锁的形成) — https://docs.oracle.com/javase/tutorial/essential/concurrency/deadlock.html
  • jcmd(Oracle 官方工具文档:jcmd 向 JVM 发送诊断命令,Thread.print 打印目标进程全部线程及调用栈,可用于定位死锁环) — https://docs.oracle.com/en/java/javase/21/docs/specs/man/jcmd.html