
1. 凌晨两点的那个告警让我重新翻开了操作系统里的死锁这一章凌晨两点告警电话把整个值班群炸醒订单服务在没有任何明显流量高峰的情况下全面超时接口耗时从 80 毫秒涨到 30 秒然后直接打满。诡异的是 CPU 曲线平静得像一潭水内存也稳机器没重启日志停在某几行之后再无输出。这种什么都不做但也不干活的状态就是死锁最典型的外在表现。我在终端敲下jstack几秒钟之后屏幕上跳出几个字Found one Java-level deadlock。两个线程各自抱着对方需要的锁静静地耗着谁也不肯先松手。死锁这个概念几乎是每一本操作系统教材都要单开一章讲的内容从汤小丹版教材到国际上那本经典的操作系统概念从考研复习笔记到期末复习提纲四个必要条件、银行家算法、资源分配图这些词背起来并不难。难的是真到线上出了问题能不能在十分钟内把现象和课本里的模型对上号能不能从一堆线程转储里揪出那条循环等待链。理论课上把它当考点工程里它是真金白银的故障数据库层面还叠着一层更隐蔽的行锁等待。这篇内容我打算分两条线并着走。一条是原理线死锁为什么会产生、四个必要条件各自意味着什么、资源分配图怎么画、银行家算法那本账到底怎么算。另一条是实操线怎么用一段代码亲手复现死锁、jstack 和数据库死锁日志分别怎么看、hung_task 这类内核检测器在干什么、线上真正落地时应该优先选预防还是检测。写这些东西不是给考试用的是给那些迟早要在凌晨两点面对同样告警的人用的。如果你正在啃操作系统课程或者天天写并发代码、管着 InnoDB 实例接下来这些内容能省下你不少翻文档的时间。2. 死锁到底卡在哪里把四个必要条件讲成人话2.1 死锁、饥饿、活锁三个长得很像但病灶完全不同的家伙这三个词经常被混在一起谈其实病灶完全不同处理手段也完全不同。死锁是指一组进程或线程各自持有资源又在等待组内其他成员释放资源形成一个闭环最后整个组谁也动不了。它是集体僵持是静态的、稳态的如果不干预它可以一直持续到进程被杀掉为止。饥饿则是另一种画风系统整体并没有卡住别的进程照常跑只是某个倒霉蛋因为优先级低、或者每次都被别人抢先长时间拿不到资源。它没有循环等待可能只是调度策略不公平。字节流传过一句话死锁是大家一起等死饥饿是就你一个人在等。现实里优先级反转、读写锁里写线程被源源不断的读请求压死都是典型的饥饿场景。活锁最容易让人误判为系统正常。进程其实一直在动不停重试、不停退让但因为重试策略设计得过于礼貌每次都恰好和对方撞上导致没有任何一个能真正向前推进。网络协议的指数退避如果参数设得诡异或者并发实现里的 CAS 重试逻辑存在对称性都可能掉进活锁。它的排查比死锁更难因为线程状态是 RUNNABLE 而不是 BLOCKED看栈看不出卡住的迹象。区分这三者有个很实用的口诀看整体吞吐有没有归零死锁、看是不是只有个别任务拿不到资源饥饿、看任务是不是在空转但没进展活锁。我现在遇到服务卡住的告警第一步就是先把这三类在脑子里过一遍能省掉大量无效排查。2.2 四个必要条件只要破掉一个死锁就发生不了死锁的发生必须同时满足四个条件缺一不可。这句话是整章内容的骨架记住了它后面所有的预防手段其实都是我怎么破坏其中某一条。互斥条件资源同一时刻只能被一个进程占用。打印机的例子最直观一段纸进去另一段就出不来。这是资源本身的属性大多数情况下你没法改。请求与保持占有并等待进程已经拿到了至少一个资源同时又在申请新的资源并且不释放手里已有的。这是最容易被自己制造出来的条件很多糟糕的代码都是这么写的。不可剥夺不可抢占资源只能由持有者主动释放别人抢不走。操作系统层面很少强行抢锁因为抢了之后持有者的中间状态就废了。循环等待存在一条进程与资源的等待环P1 等 P2 手里的资源P2 等 P3P3 又回过头等 P1。四个条件里真正在工程中有可操作空间的是后两个。请求与保持可以通过一次性申请全部资源来破坏代价是资源利用率暴跌不可剥夺可以通过tryLock加超时释放来实现循环等待最常被破坏也就是所有人嘴里说的全局锁顺序。互斥条件基本不动因为它是资源定义的一部分。我见过不少人一上来就说我们上分布式锁就不会死锁了这是个误解。分布式锁把互斥搬到了更远的地方循环等待照样存在只是从进程内搬到了集群间排查难度还翻了好几倍。2.3 资源分配图拿一张图判断到底有没有死锁资源分配图是理解死锁最省力的工具画法很简单圆圈代表进程方框代表资源类型方框里的小圆点代表该类资源的实例个数。从方框指向圆圈的是分配边表示这个实例已经给出去了从圆圈指向方框的是请求边表示这个进程正在等这个资源。判断规则分两种情况资源类型图中出现环结论每类资源只有一个实例有环一定死锁每类资源只有一个实例无环一定没有死锁每类资源有多个实例有环可能死锁需要进一步分析每类资源有多个实例无环一定没有死锁多实例有环不一定死锁这一点很多人在做题时会栽跟头。原因是环上的某个进程可能等到的是另一个实例而那个实例并不在环里被占着于是它可以继续往前走环自然就解开了。这道题的判定要靠化简找一个请求能被满足的进程假设它完成后释放全部资源再回头满足别人直到图被简化完。如果所有进程都能被消化说明没有死锁。这套化简方法其实就是后面银行家算法里安全性检查的前身。理解它比死记结论重要得多因为线上排查时你脑子里的等待图就是这张资源分配图的简化版本只不过节点换成了线程 ID边换成了锁的持有关系。3. 亲手造一个死锁从代码复现到现场取证3.1 两把锁交叉加锁最经典的死锁模板课本上的模型太抽象我更喜欢直接写一个必然会死锁的程序。最经典的模板是两把锁反向加锁代码大概长这样public class DeadLockDemo { private static final Object lockA new Object(); private static final Object lockB new Object(); public static void main(String[] args) { Thread t1 new Thread(() - { synchronized (lockA) { System.out.println(t1 got lockA); sleepQuietly(200); synchronized (lockB) { System.out.println(t1 got lockB); } } }, Thread-A); Thread t2 new Thread(() - { synchronized (lockB) { System.out.println(t2 got lockB); sleepQuietly(200); synchronized (lockA) { System.out.println(t2 got lockA); } } }, Thread-B); t1.start(); t2.start(); } private static void sleepQuietly(long ms) { try { Thread.sleep(ms); } catch (InterruptedException ignored) { } } }这个程序的死锁是百分之百会触发的。t1 拿到 lockA 之后睡 200 毫秒t2 趁这个窗口拿到 lockB然后两边同时去申请对方手里的锁卡死。中间那个 sleep 的作用是放大时间窗口让两个线程都能先拿到第一把锁。如果没有它很可能一个线程瞬间跑完两个加锁死锁根本复现不出来这也是很多人在本地测试时看不到死锁的原因。这段代码有两个值得琢磨的点。第一它完美对应了四个必要条件lockA 和 lockB 互斥两个线程都持有了一把又去要另一把请求与保持锁不能被人抢走不可剥夺A 等 B、B 等 A 构成循环等待。第二休眠时间的选择有讲究太长会让死锁窗口过于明显、失去代表性太短又复现不出来200 毫秒左右是个经验值。注意这段代码一旦卡死JVM 不会自己恢复。想跑第二次必须重新启动进程或者提前给线程设置好中断逻辑和超时锁。3.2 jstack 现场取证线程转储里那几行关键信息程序卡住之后用jps找到进程号然后执行jps -l jstack -l 12345 /tmp/deadlock.txt转储文件里会出现一段非常明确的提示Found one Java-level deadlock: Thread-B: waiting to lock monitor 0x00007f8c1c0a3d08 (object 0x000000076ab0e908, a java.lang.Object), which is held by Thread-A Thread-A: waiting to lock monitor 0x00007f8c1c0a3cf8 (object 0x000000076ab0e8f8, a java.lang.Object), which is held by Thread-B Java stack information for the threads listed above: Thread-B: at com.example.DeadLockDemo.lambda$main$1(DeadLockDemo.java:26) - waiting to lock 0x000000076ab0e908 (a java.lang.Object) - locked 0x000000076ab0e8f8 (a java.lang.Object)读这段输出有三个要点。第一个是which is held by这行它直接告诉你这条等待链的下一个节点是谁顺着往下读就能把整个环拼出来。第二个是- locked 地址 和 - waiting to lock 地址前者是线程当前持有的锁的地址后者是它正在等的锁的地址两个地址在两条线程栈里互相出现环就闭合了。第三个是行号信息它会精确指到DeadLockDemo.java:26也就是第二层synchronized的位置这比通读业务代码快得多。有个很容易被忽略的坑jstack 对synchronized关键字实现的监视器锁识别得很好对ReentrantLock这类基于 AQS 的锁则往往不给死锁提示。如果你用的是显式锁转储里只能看到一堆 WAITING (parking) 的线程得自己对着栈去推谁等谁。这也是为什么我建议在排查时同时打开-XX:PrintConcurrentLocks或者干脆用jcmd pid Thread.print -l输出信息更全一些。如果一次抓取没抓到可以间隔几秒连抓三次。判断死锁和判断长时间任务的关键区别在于三次转储之间线程栈完全没变、等待地址也完全没变那基本就是死锁了。3.3 数据库那一侧InnoDB 的行锁死锁更难抓应用层的死锁有 jstack 兜着数据库死锁就没那么友好了因为它的现场在服务端。MySQL 里最常见的场景是两个事务反向更新同一批行-- 会话 A BEGIN; UPDATE t_order SET status 1 WHERE id 100; UPDATE t_order SET status 1 WHERE id 200; -- 会话 B BEGIN; UPDATE t_order SET status 1 WHERE id 200; UPDATE t_order SET status 1 WHERE id 100;A 拿了 id100 的行锁去要 200B 拿了 200 的行锁去要 100两条事务的等待图里出现了环。此时 InnoDB 的死锁检测器会介入主动回滚其中一个事务客户端收到ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction这个报错有个重要特征它不是你写错了 SQL而是两个事务的访问顺序不一致。所以业务代码里必须做好重试捕获 1213 之后退避重试通常一两次就能成功。很多人第一次见到这个错误会去找 DBA 投诉其实是代码层面的顺序问题。想看完整现场执行SHOW ENGINE INNODB STATUS\G输出里找LATEST DETECTED DEADLOCK这一段它会列出两个事务分别持有什么锁、在等什么锁、执行了哪条 SQL、以及最后决定回滚谁。这段信息比报错本身值钱得多因为它直接告诉你哪两条 SQL 的顺序需要统一。SQL Server 这边的排查路径不太一样1222 是死锁错误码可以通过扩展事件或者打开 trace flag 1222 把死锁图写进错误日志system_health 会话里也会留存阻塞报告。Oracle 的死锁报 ORA-00060会在告警日志里生成 trace 文件。不同产品的日志格式差异很大但核心信息永远是同一套谁持有、谁等待、等待的对象是什么。只要抓住这条主线换哪个数据库都能读。4. 四条破局路线预防、避免、检测、解除4.1 死锁预防从四个条件里挑一个下手死锁预防的思路很直接让四个必要条件里至少一个永远不成立死锁就发生不了。破坏请求与保持的经典做法是一次性申请全部资源也就是进程启动时把需要的资源全要到手中途不再申请。这在批处理时代可行放到今天几乎没有实用性因为并发服务真正需要的资源规模根本没法提前枚举而且资源利用率会被压到极低。另一种做法是申请新资源前先释放已有的代价是状态可能被破坏需要回滚机制兜底。破坏不可剥夺在锁的语境下就是申请不到就主动放手。ReentrantLock.tryLock(timeout)是这把钥匙超时拿不到就释放自己已持有的锁退回去重试。这种做法能有效打断循环等待但要注意释放锁意味着中间状态可能被破坏业务上必须能安全回滚否则你只是把死锁换成了数据错乱。破坏循环等待在工程里最常用也就是所谓的全局锁顺序。给所有锁编号任何线程都必须按编号从小到大加锁。上面那个死锁例子里只要规定 lockA 的编号小于 lockBt2 也必须先拿 lockA 再拿 lockB环就永远成不了。提示按主键顺序批量更新、按 ID 排序后再逐条加锁本质上就是全局锁顺序在数据库场景的翻版。这是解决数据库死锁最有效的一招比任何配置参数都管用。4.2 死锁避免银行家算法那本账到底怎么算避免和预防的区别在于预防是从设计上让死锁不可能发生避免是每次分配资源前先算一算会不会把系统带进不安全状态。银行家算法是这类方法里最出名的名字来自银行放贷的比喻——银行手里的现金是有限的给每个客户放贷前都得评估一下剩下的钱还够不够让所有客户都完成周转。算法涉及四个数据结构名称含义Available当前各类资源的可用数量Max每个进程对各类资源的最大需求Allocation已经分配给每个进程的资源数量Need每个进程还需要多少等于 Max 减去 Allocation每次有进程提出请求 Request 时算法按三步走先检查Request Need再检查Request Available两个都通过才试着分配。分配之后把系统状态代入安全性算法看能不能找到一个安全序列也就是一串进程顺序使得每个进程都能靠当前可用资源加前面进程释放的资源完成任务。找不到安全序列就把这次试探性分配撤销让请求方继续等。拿教材里那组经典数据练一遍五进程三资源Available (3, 3, 2) 进程 Allocation Max Need P0 0 1 0 7 5 3 7 4 3 P1 2 0 0 3 2 2 1 2 2 P2 3 0 2 9 0 2 6 0 0 P3 2 1 1 2 2 2 0 1 1 P4 0 0 2 4 3 3 4 3 1安全性检查的过程是这样的当前可用 (3,3,2)只有 P1 的 Need (1,2,2) 能被满足于是假设 P1 完成归还它持有的 (2,0,0)可用变成 (5,3,2)。接着 P3 的 Need (0,1,1) 满足归还 (2,1,1)可用变成 (7,4,3)。P4 的 Need (4,3,1) 满足归还 (0,0,2)可用变成 (7,4,5)。P0 的 Need (7,4,3) 满足归还 (0,1,0)可用变成 (7,5,5)。最后 P2 的 Need (6,0,0) 满足。安全序列是 P1、P3、P4、P0、P2。这就说明当前状态是安全的。反过来如果某个状态怎么找都找不到完整序列那就是不安全状态此时不能分配。这里要特别强调一个容易搞混的点不安全状态不等于死锁状态它只意味着系统失去了保证不死锁的能力。如果一个进程还没跑完就狮子大开口或者运行中临时改变最大需求银行家算法给出的保证就失效了。这两个假设在真实系统里几乎不成立所以算法更多停留在教学和少数专用系统的层面。4.3 死锁检测与解除先放开出事再治检测这条路走的是完全相反的哲学不限制资源分配方式允许死锁发生但保留一套机制定期或实时把它找出来。单实例场景用等待图节点是进程边是我等你的关系图里出现环就是死锁多实例场景则复用前面那套化简方法能化简完就没死锁化简不动就是死锁了。检测的成本是真实的。一个几千条线程的系统如果每次都做全图检测CPU 会被吃掉一大块所以工程上通常采用定时检测加阈值触发的方式比如每 5 秒跑一次或者只在某个线程等待时间超过 10 秒时才触发一次检测。检出死锁之后就得解除手段主要是三类进程终止最简单粗暴全部回滚或者逐个回滚直到环被打开。逐个回滚时需要按代价排序优先牺牲那些中间结果少、重算成本低的进程。资源抢占从某个进程手里强行拿走资源交给别人。这里的难点不是抢是抢占后怎么把被抢进程恢复到某个能继续跑的中间状态通常要靠检查点回滚代价很高。人工干预数据库场景里很常见DBA 看完成本分析后手动 kill 掉某个会话。选择牺牲谁是个策略问题常见判断依据包括已运行时间、剩余时间、已产出结果的价值、回滚代价。教材里还会提到回滚次数这个维度避免同一个进程反复被牺牲导致饥饿。4.4 工程现实为什么绝大多数系统选的是预防为主、检测兜底理论上有四条路落到真实系统里选择其实很集中。我先说结论通用操作系统和绝大多数后端服务走的都是用锁顺序和超时做预防 用检测工具兜底的组合路线纯粹的死锁避免几乎不会出现在生产系统中。原因不难理解。死锁避免要求提前知道每个任务的最大资源需求这在动态创建线程、按需加载数据的服务里根本做不到。检测机制虽然可行但需要维护全局的等待图和实例可用量并发开销和数据一致性都是麻烦。相比之下预防里的资源有序分配成本极低只需要在编码规范里约定一条规则就可以从源头上掐掉大部分循环等待。数据库领域的实践也印证了这一点。InnoDB 默认开启死锁检测检测到就回滚代价较小的事务这是一种典型的检测加解除。但真正成熟的业务代码不会依赖引擎兜底而是通过固定访问顺序、缩短事务、减少锁范围来主动预防。两层一起上才是现实中的稳定状态。5. 排查手册从现象到根因的完整链路5.1 第一步永远是保现场别急着重启我踩过的最大的坑就是第一次遇到服务卡死时手忙脚乱直接重启了。重启之后服务确实恢复了但死锁现场彻底消失事后复盘时只能靠猜。保现场这件事的优先级高于恢复服务尤其是那种不是几百个用户同时在骂的场景多花三分钟收集信息是值得的。收集清单我总结成这几项信息类型获取方式价值线程转储jstack -l pid间隔 5 秒连抓三次判断是否真死锁、还原等待环堆信息jmap -histo:live pid排查是否因内存问题间接引发锁等待GC 日志提前配置好的 GC 日志文件排除长时间 STW 造成的假死现象数据库锁等待SHOW ENGINE INNODB STATUS、快照视图定位行锁等待链内核阻塞任务/proc/pid/task、hung_task 日志排查栈底层的不可中断等待三次线程转储的对比是判断死锁最有效的手段。如果三次都是同一批线程、同样的等待地址、同样的栈顶方法基本可以定案。如果栈顶方法在变那多半是慢而不是死方向要转到性能排查上去。5.2 分层定位从应用栈一路往下挖排查死锁最忌讳的就是一头扎进业务代码里通读。我习惯按层来每层只问一个问题。应用层问的是谁在等谁。打开线程转储找 BLOCKED 和 WAITING 状态的线程看它们的等待地址有没有互相引用。这一步能解掉八成以上的线程死锁。框架层问的是线程池是不是被打满了。有时候应用层的锁没问题但线程池队列满了新任务进不来看起来也像整体卡死。这时候看的是线程池的活跃线程数、队列长度、拒绝策略。真正的死锁往往只涉及少数几个线程而线程池耗尽通常是一大片线程都在等待同一个资源。数据访问层问的是有没有慢 SQL 或者锁等待。数据库侧的锁等待会以超时的形式暴露到应用层表象和死锁很像。快速判断方法是看数据库的当前等待事件如果大量会话卡在行锁或者元数据锁上问题就在数据层。内核层问的是有没有不可中断的睡眠。D 状态进程是最难处理的一类它连kill -9都不响应因为进程卡在内核态的系统调用里。Linux 提供了 hung_task 检测机制当某个任务处于不可中断睡眠超过kernel.hung_task_timeout_secs默认 120 秒时内核会打印告警栈里能看到它卡在哪个内核函数上。如果怀疑是内核锁问题可以打开 lockdep 检查器它能给出锁的依赖关系图直接指出潜在的死锁路径。这套分层排查的顺序很关键永远从最上层的应用栈开始因为那里的信息最完整、修复成本最低。直接跳到底层去查内核锁往往是绕远路。5.3 一次完整的案例复盘订单服务的死锁是怎么被干掉的回到开头那次告警我把整个排查和修复过程记录下来作为一个完整的参照。现象订单创建接口大面积超时成功率从 99.9% 掉到 12%CPU 使用率只有 15%无重启记录。第一轮取证连抓三次线程转储发现order-service线程池里有两组线程长期处于 BLOCKED等待的锁对象地址互相出现栈顶都是OrderServiceImpl.updateStatus。定位到方法发现它在同一个事务里先更新订单状态再更新库存记录两个不同的加锁顺序在两条代码路径上被使用了。第二轮取证查数据库SHOW ENGINE INNODB STATUS里有一段 LATEST DETECTED DEADLOCK确认了两条 UPDATE 语句正好按相反顺序命中同一批行。根因一个是订单状态流转任务按订单在前、库存在后的顺序加锁另一个是库存回滚任务按库存在前、订单在后的顺序加锁。两者在并发场景下必然交叉。修复统一加锁顺序全部按业务主键的字典序访问同时给这两条路径加上独立的锁对象把两个任务串行化牺牲一点并发度换取确定性。上线后同一路径再没出现过死锁。后续加固加了线程池监控指标把 BLOCKED 线程数纳入告警数据库事务的捕获逻辑里加了 1213 错误的自动退避重试代码评审清单里加了一条检查跨表更新的加锁顺序。这个案例最值得记的不是修复手段而是它暴露的是一个设计问题而不是一个代码 bug。两条路径单独看都没有错错在它们共存。这类问题只能靠规范约束来防。5.4 常见问题速查表现象可能原因定位手段处置方式接口全量超时、CPU 很低线程死锁jstack 连续抓三次比对定位环后统一锁顺序重启恢复只有个别请求一直挂着饥饿或锁被长时间占用看线程状态和锁持有者优化锁粒度提高该路径优先级线程 RUNNABLE 但业务无进展活锁或自旋重试看 CPU 和重试次数调整退避策略引入随机化报 Deadlock found when trying to get lockInnoDB 行锁死锁SHOW ENGINE INNODB STATUS统一访问顺序加自动重试进程处于 D 状态、kill 无效内核态不可中断等待hung_task 日志、内核栈定位内核对端设备或内核锁重启后问题立即消失大概率是锁相关问题靠事前监控和日志留存提前配置好转储与慢查询日志这张表我在排查时基本会过一遍它最大的价值是把猜变成排除。每排除一项剩下的可能性就少一块。6. 几条用血换来的经验以及一些往深处走的建议6.1 编码阶段就该养成的防御习惯第一条也是最重要的一条任何需要加锁的地方先问自己能不能不加锁。无锁队列、不可变对象、线程封闭、单线程事件循环这些手段能解决的问题远比想象中多。锁用少了死锁的概率自然就下来了。我在重构时经常发现某些锁只是因为在多线程里共享了一个可变集合换成不可变快照加原子引用替换锁就完全不需要了。第二条是加锁顺序必须全局唯一。这条规则要写进团队规范而不是靠个人自觉。具体做法是给所有可能被一起持有的锁建立一个编号表任何代码路径都按编号从小到大加锁。数据库层面同理跨表更新按主键排序后再执行。看起来笨但它能挡住绝大多数循环等待。第三条是永远给锁设超时。synchronized没有超时机制所以在我手里需要跨模块持有的锁基本都会用ReentrantLock.tryLock(timeout)实现。超时之后的做法不是直接抛异常了事而是记录现场、释放已持有的锁、退避重试。这一点在分布式锁上尤其重要锁服务抖动时如果没有超时整个业务线程池可能瞬间被拖垮。第四条是把锁的范围压到最小。很多人习惯在方法签名上加synchronized或者在事务里塞进远程调用、文件读写这类耗时操作。锁持有时间越长别人等待越久交叉的机会越大。我通常要求锁内代码不能包含网络请求、不能包含 IO、不能在循环里反复加解锁。6.2 上线之后的观测能力建设死锁的可怕之处在于它不报错只是不动。所以事前的观测能力比事后排查更值钱。线程层面的监控我会关注线程池里 BLOCKED 状态的线程数、平均等待时长、队列积压长度这三个指标。正常的服务 BLOCKED 线程数应该长期接近于零一旦持续大于某个阈值就该告警。JVM 层面可以打开-XX:PrintConcurrentLocks让线程转储带上锁信息如果用了显式锁还可以考虑开 JFR 采集锁竞争事件事后能在飞行记录里回看。数据库层面慢查询日志、锁等待超时时间、以及死锁检测的频率都值得配置好。把innodb_print_all_deadlocks打开让每次死锁都落盘这样即使当时没人值守事后也能复盘。内核层面hung_task 的检测是默认开着的但默认值 120 秒对在线服务来说太长敏感业务可以调到 30 秒左右让不可中断睡眠尽早暴露。同时要保证内核日志能被采集走否则检测器吭哧吭哧打了日志没人看等于没配。这些能力建设最容易被推迟理由是现在没出问题。可一旦真出问题有没有这些东西决定了你是十分钟解决还是通宵定位。6.3 如果你正在准备操作系统这门课的考试把这部分单独拉出来说是因为死锁几乎是必考内容而且考法有规律可循。四个必要条件、资源分配图的画法与判定、银行家算法的安全性检查这三块是高频考点。复习时不要只背结论一定要动手算一遍安全序列把 Need 矩阵列出来按可用量逐一尝试满足。计算过程本身就是答案写清楚每一步可用量怎么变的就算最后序列选错了也能拿到大部分分数。概念辨析题也经常出现比如死锁和饥饿的区别不安全状态和死锁状态的关系这类题的关键是把边界讲清楚不安全状态只是失去了保证不等于已经死锁饥饿没有环只是分配不公平。把这些区分点记住比背一大段定义管用。还有一个容易丢分的点是死锁检测与解除的策略分析。题目通常会问终止哪个进程代价最小答题时要从已运行时间、剩余工作量、已产生结果的价值、回滚代价几个维度展开别只写一句终止优先级最低的。判卷老师看的就是你有没有把权衡逻辑说清楚。6.4 我在实际使用中的体会折腾了这么多年我对死锁最大的感受是它是一个设计问题伪装成的运维问题。绝大多数线上死锁代码单独看都没毛病问题出在跨模块的资源访问顺序没有统一约定或者某个共享状态的锁范围被人无意识地放大了。工具能帮你定位但真正解决问题靠的是规范。另外一点不要迷信重启大法。重启确实能恢复服务但它把根因一起清掉了。我现在遇到这类问题的习惯是先花两分钟抓现场再恢复服务等业务平稳后回来看转储。两分钟的成本换来的是下次不再掉同一个坑。如果这块内容你想继续往深走有两个方向值得投入。一个是线程转储的深度阅读能力能读懂各种锁状态和栈帧背后的含义这项技能在排查性能问题时同样好用。另一个是数据库并发控制从行锁、间隙锁到隔离级别把 InnoDB 那套机制摸透之后你会发现应用层的很多死锁问题其实是数据库模型设计不当造成的。