尧图精选

不安全状态≠死锁:银行家算法与死锁避免的深度解析

🕒 发布时间:2026/10/1 18:09:06 📁 来源:尧图网络
我用一个课堂上的真实场景来切入这个话题。讲操作系统原理时每次讲完银行家算法和死锁那一章总有学生追着问一个问题老师既然不安全状态这么危险为什么书上说它不一定会变成死锁那岂不是我们前面学了一大堆判断安全状态的东西都白费了这个问题问得特别好。因为它恰恰是理解死锁、安全状态、银行家算法三者关系的关键枢纽。很多人学完这一章背下了“死锁的四个必要条件”背下了“银行家算法四个步骤”但遇到填空题“不安全状态是否必然导致死锁”时还是会犹豫。今天我就把这层关系彻底掰开揉碎讲清楚从理论定义到实际代码再到生产环境里怎么用这个结论排查问题一次性讲透。先说结论后面展开论证不安全状态只是死锁的必要条件不是充分条件。换句话说从“不安全状态”到“死锁”之间还隔着一道“资源请求运气”的屏障。系统处于不安全状态时确实存在某个未来的资源申请序列可能引发死锁但操作系统不会预先拒绝所有请求也不会强制回收资源所以只要实际执行时进程们“没有踩中”那个最坏序列系统就能继续运行下去永远不让死锁真正发生。这篇文章适合正在复习操作系统期末考的在校生、准备面试的考研党、刚接触并发编程想弄懂死锁原理的开发者以及工作中用 jstack、数据库监控排查过死锁但知其然不知其所以然的工程师。我会从死锁的严格定义开始讲起把安全状态与不安全状态的关系、银行家算法的本质、以及“明明不安全但没死锁”的典型场景全部拆解一遍最后再给出一套可在 Linux 环境里复现的模拟实验以及生产环境的排查清单。1. 死锁的定义与四个必要条件先把“死锁是什么”钉死1.1 死锁四大条件的严格表达死锁不是“程序卡住了”这么简单。教科书上对死锁的严格定义是一组进程中的每个进程都在等待一个只有该组中其他进程才能引发的事件通常是释放所占资源则该组进程发生了死锁。要让死锁成立必须同时满足以下四个条件互斥条件资源在同一时刻只能被一个进程占用。比如打印机、磁带机这类不可共享的资源。持有并等待条件进程已经持有了至少一个资源又在等待获取额外的资源而它等待的资源正被其他进程持有。不可剥夺条件进程已获得的资源在该进程主动释放之前不能被其他进程强行抢占。循环等待条件存在一个进程等待环路即 P0 等待 P1 占有的资源P1 等待 P2 占有的资源……Pn 等待 P0 占有的资源。这四个条件是死锁的必要条件不是充分条件。也就是说只要不发生死锁这四个条件必然至少有一个不成立但只要这四个条件同时满足系统不一定马上死锁——它们只是构成了死锁所必需的环境土壤。这个“必要但不充分”的性子其实是整个死锁理论的底层基调。1.2 为什么死锁要求“不可剥夺”而不是“绝对不能剥夺”很多人初学时会想既然不可剥夺可能导致死锁那设计操作系统时把不可剥夺去掉改成所有资源都能强制回收不就行了这里要区分两个概念资源本身的物理属性和操作系统施加的调度策略。打印任务做到一半时强拆打印机打印出来的是一堆废纸数据库对某一行加锁做事务更新时如果系统贸然把锁剥走事务的原子性就被破坏数据可能处于中间状态。因此“不可剥夺”通常是资源在逻辑上的硬约束而不是系统不想管。死锁理论里还有一条路叫“资源剥夺法”代价是回滚进程到安全点成本极高所以一般放在死锁检测之后才用。理解了这四个条件的权重我们再来看安全状态和不安全状态——它们描述的不是“现在是否满足四个条件”而是“系统是否预留了避免死锁的逃生通道”。2. 安全状态、不安全状态与死锁状态的三角关系2.1 安全状态系统能保证所有进程最终完成先给一个数学化的定义。如果系统能按某种顺序称为安全序列为每个进程分配其所需的全部资源直到所有进程都顺利结束那么系统就处于安全状态。这里的“能”指的是资源分配可以逐步推进每一步之后系统仍有足够资源满足下一个进程的最大需求。光说定义太抽象看一个经典例子。系统中有 12 台磁带机三个进程 P0、P1、P2 的最大需求分别是 10 台、4 台、9 台。当前已分配数分别是 5 台、2 台、2 台剩余可用 3 台。此刻 P1 还差 2 台就能满足最大需求而可用数正好是 3 台所以先把 2 台给 P1P1 跑完释放 4 台原本持有的 2 台加上新分配的 2 台可用变成 5 台。接着轮到 P0它还差 5 台正好够P0 跑完释放 10 台可用变 15 台。最后 P2 还差 7 台15 台足够。P1→P0→P2 就是一个安全序列。所以系统当前处于安全状态。2.2 不安全状态找不到安全序列但并非一定死锁如果把上例预设改一下系统可用资源还是 3 台P2 当前已分配从 2 台改成 3 台那么 P2 还差 6 台。此时可用 3 台只够满足 P1差 2 台不够满足 P0差 5 台也不够满足 P2差 6 台。先给 P1P1 释放后可用变 5 台但 P0 要 5 台、P2 要 6 台只能满足 P0。如果调度器先把资源分给了 P0P0 跑完释放后系统还能继续但如果某个时刻 P0 没有申请资源而是 P1 继续占着资源不放或者 P0 提前申请了一部分又进入等待局面就僵住了。关键在于找不到安全序列只是说明“存在一种可能导致死锁的资源分配路径”而不是说“无论如何都会死锁”。这就是不安全状态与死锁状态之间的本质区别。2.3 三者包含关系死锁状态 ⊂ 不安全状态 ⊂ 全部状态用集合的语言来记就非常清晰死锁状态一定是不安全状态。不安全状态不一定是死锁状态。安全状态一定不会死锁。我见过一个很形象的类比安全状态是“路还宽怎么开都不会堵死”不安全状态是“已经进入单行道但前方不一定会堵车只是存在堵死的可能”死锁状态是“四个方向的车全顶在一起谁也动不了”。你在路口看到黄灯闪烁不安全不代表事故一定发生死锁完全可能一路顺风开过去——但交通管理员银行家算法为了安全起见倾向于在更早的阶段就拦下那些可能进入单行道的车辆。3. 为何不安全状态不一定演化为死锁四个层面的拆解3.1 安全序列不存在≠实际执行序列会踩雷银行家算法判断安全性时会假设每个进程最终会“请求它的最大需求”并据此寻找一种能够完全满足所有进程的推进顺序。如果存在至少一种顺序系统就是安全的反之则不安全。但问题在于进程实际的资源申请顺序未必按最大需求请求也未必按照算法预设的序列推进。举个具体场景P0 最大需求是 10 台磁带机但它这次可能只申请 1 台用完就释放峰值消耗只有 5 台远未触及最大需求。即便系统因为找不到“能让所有进程同时按最大需求跑完”的序列而被判定为不安全实际中 P0 根本没有索取那么多资源自然也就不会构成资源竞争的临界点。这种“预留空间远大于实际占用”的现象在不安全状态不转为死锁的原因里非常常见。3.2 资源释放时机与公式化条件死锁真正发生的必要条件之一是循环等待。而不安全状态中我们找不到保证所有进程完成的安全序列但进程之间的等待关系可能根本不构成环。假设三个进程都不再持有额外资源、只是各自等一个外部事件如用户输入那么它们不存在资源层面的循环等待当然不会死锁。再比如某个进程虽然处于不安全状态清单里但它可能在极短时间内就运行完毕、主动释放资源从而把系统重新拉回安全状态。这就好比一个拥挤的十字路口没有红绿灯不安全但车辆因为通行有序并没有卡死。这里有一个可以转化为考题的判定在不安全状态下只有当后续资源请求顺序恰好使得循环等待条件成立死锁才会真正发生。所有把“不安全状态”直接等价于“死锁”的说法都是在忽略“实际请求序列”这个变量。3.3 操作系统不一定会“提前下手”既然不安全状态这么危险为什么系统不直接拒绝所有请求呢如果操作系统在发现自己即将进入不安全状态时就强制拒绝本次资源分配系统就永远不会发生死锁——这就是银行家算法的核心思想通过安全性检查把所有资源分配都限制在安全状态内。但请注意银行家算法是死锁避免的一种策略并非所有操作系统都在使用。实际系统中Linux、Windows 默认并不为所有资源做全量的银行家式安全性检查因为进程的资源需求表难以精确预知资源类型过多时检查开销太大。操作系统更常用的是死锁预防破坏四个必要条件之一与死锁检测允许死锁发生检测到后解除。因此大部分情况下系统一旦进入不安全状态并没有一套“哨兵程序”立刻拦截后续请求——死锁的演化完全取决于接下来进程们怎么申请资源。如果操作系统不做预防和避免又觉得死锁发生概率低、检测和恢复代价更小就会选择“鸵鸟算法”把头埋进沙子里假装没看见等到真卡死时再人工介入重启或杀掉进程。这种策略虽然极端但也侧面印证了一件事系统允许自己处于不安全状态而不安全状态大多数时候并不会演化成死锁。3.4 资源分配策略的“伪随机性”给了系统喘息空间在不安全状态中资源分配的时机、请求资源的进程数量、释放资源的次序都存在大量偶然性。一个进程可能申请资源 A 失败后转而执行无需资源 A 的代码片段从而错开了与其他进程的竞争窗口也可能因为调度器的时间片轮转某进程在等待时被调度下去另一个进程恰好完成并释放了关键资源打破了潜在的循环等待。我见过一个真实的并发程序案例系统总资源足够但设计上有三把锁线程 A 持有锁 1 请求锁 2线程 B 持有锁 2 请求锁 3线程 C 持有锁 3 请求锁 1理论上这是教科书式的循环等待死锁必然发生。但程序跑了几周才死锁一次原因就是三个线程同时到达锁请求点的概率极低多数时候总有一个线程先释放了锁等待链被打断。这正是“不安全状态”与“实际死锁”之间概率鸿沟的生动写照。4. 从理论到实操银行家算法为啥比想象中更谨慎4.1 银行家算法的核心以“未来可能的最大要求”为判断依据银行家算法Bankers Algorithm在死锁避免领域占据核心地位。名字由来与银行放贷相似银行不会因为客户当前存款不足就拒绝所有贷款而是评估“如果所有客户同时提取最大额度的存款银行是否仍然足以兑付”。如果可以就放贷如果不行就不放贷。其数据模型包含四个矩阵/向量Available各类资源的剩余可用数。Max每个进程对各类资源的最大需求。Allocation每个进程当前已分配到的资源。Need每个进程还需要的资源数Need Max - Allocation。安全性检查算法维护一个Work向量系统当前可提供的资源数和一个Finish数组各进程是否已结束不断寻找一个Finish[i] false且Need[i] ≤ Work的进程来推进假设分配、回收资源。若所有进程都能被标记为Finish true则系统安全否则不安全。4.2 一个具体的安全性检查手算过程假设系统中有 A、B、C 三类资源数量分别为 10、5、7。五个进程 P0 到 P4 的最大需求和已分配情况如表进程Allocation (A B C)Max (A B C)Need (A B C)P00 1 07 5 37 4 3P12 0 03 2 21 2 2P23 0 29 0 26 0 0P32 1 12 2 20 1 1P40 0 24 3 34 3 1Available初始为 3 3 2。逐步执行查找 Need ≤ Work 的进程。P1 的 Need 是 1 2 2Work 是 3 3 2满足假设 P1 完成并释放资源Work 变为 5 3 2。P3 的 Need 是 0 1 1满足释放后 Work 变为 7 4 3。P4 的 Need 是 4 3 1满足释放后 Work 变为 7 4 5。P0 的 Need 是 7 4 3满足释放后 Work 变为 7 5 5。P2 的 Need 是 6 0 0满足释放后 Work 变为 10 5 7。所有进程都能完成系统处于安全状态。如果换一种分配假设连一个Need ≤ Work的进程都找不到系统就是不安全状态此时银行家算法会拒绝新的资源请求。注意这里的“假设推进”是算法的试探并不是真正把资源分配给了进程——这也是为什么银行家算法能预测死锁风险而不是等死锁发生后再处理。4.3 银行家算法的局限为什么实际用得不多银行家算法理论上很完美但在真实操作系统中应用范围有限原因有三进程最大需求难以预知一个进程在运行前并不总能准确声明自己会用到多少资源尤其是文件、网络连接这类动态资源。资源数量可能动态变化银行家算法假设资源总量固定但现代系统可能动态添加磁盘、内存等资源。计算复杂度与维度爆炸进程数和资源类型增多后安全性检查的计算开销不可忽视而且每笔资源请求都要检查一次严重影响系统吞吐。因此银行家算法多用于教学、车辆调度、信贷评估等有限资源、可知需求的场景以及一些对可靠性要求极高的嵌入式系统中。普通服务器上更常见的是死锁预防和检测恢复。5. 动手验证构造一个“不安全但不死锁”的实验纸上谈兵终究不够我在 Linux 环境里用 Python 写过一个模拟程序用来帮助学生理解“不安全状态不必然死锁”。这个实验的思路是让程序根据银行家算法在某个时刻判定系统为不安全状态但随后通过控制进程实际的资源申请顺序让系统最终全部跑完不死锁。5.1 实验设计系统有 1 种资源总量为 5。三个进程 A、B、C 的 Max 分别为 3、4、2。初始 Allocation 都设为 0Available 是 5。按银行家算法判定此时可用 5五个进程其实是三个进程的 Need 分别是 3、4、2依次分配都没问题系统安全。接下来我让进程 A 先申请 3 个资源系统进入分配状态此时 Available 变为 2。如果此时进程 B 申请 2 个资源则系统 Available 变 0没有资源可再分配安全性检查时所有进程的 Need 都无法被满足判定不安全。但实验故意让进程 B 没有申请资源而是让进程 A 立即运行完毕并释放全部 3 个资源。系统 Available 变回 5一切恢复正常。整个过程虽然一度被算法判定为“不安全状态”但死锁从未发生。5.2 核心模拟代码import threading import time # 模拟只有一个资源的系统resource_pool 是剩余资源数 resource_pool 5 lock threading.Lock() def process(name, need, hold): global resource_pool print(f{name}: 需要 {need} 个资源) with lock: if resource_pool need: resource_pool - need print(f{name}: 成功申请 {need} 个资源剩余 {resource_pool}) else: print(f{name}: 申请失败剩余只有 {resource_pool}) return # 模拟运行一段时间后释放 time.sleep(1) with lock: resource_pool hold print(f{name}: 释放 {hold} 个资源剩余 {resource_pool}) threads [ threading.Thread(targetprocess, args(A, 3, 3)), threading.Thread(targetprocess, args(B, 2, 0)), # B 只需要但未申请 threading.Thread(targetprocess, args(C, 2, 0)), ] for t in threads: t.start() for t in threads: t.join() print(最终资源剩余:, resource_pool)这段代码里线程 B 和 C 的 hold 故意设为 0模拟“虽然声明了最大需求但实际并没有真正占用很多资源”的情形。运行后你会发现即使 A 成功申请资源后一度让 Available 较小B 和 C 也没有形成循环等待而是 A 先释放系统顺利收尾。你还可以把 B 和 C 的申请时间往前调整让三者同时卡在申请处复现一个真正的死锁现场——对比之下才能理解“状态判定”和“实际推进”之间的错位。5.3 实验结论这个实验说明了两个关键点银行家算法的安全性判断是基于最大需求假设的悲观判断。它为了绝对安全宁可不批准可能造成不安全的请求。但现实世界中进程不会总是按最大需求申请资源所以“不安全”只是一种风险预警不是死亡判决。死锁是否发生取决于进程的实际资源请求序列以及资源释放的时机。不安全状态给出的是“可能死锁”的集合真正的死锁是这个集合中某些特定路径的组合。6. 真刀真枪生产环境中如何判断和排查死锁学完理论我们再把这些知识映射到真实的开发与运维场景中。生产环境里我们更多考虑的不是“系统会不会进入不安全状态”而是“如果已经发生死锁如何快速定位、如何在设计阶段就避免”。6.1 线程死锁排查jstack 的典型用法Java 应用是死锁高发区。当线上接口突然无响应且 CPU 占用率很低时第一个怀疑对象就是线程死锁。排查步骤很简单jps找到应用的进程 PID。jstack PID thread_dump.txt导出线程快照。搜索Found one Java-level deadlock关键字。如果存在死锁jstack 会直接打印出死锁涉及的线程、锁对象、堆栈信息。一个典型的死锁 dump 片段长这样Found one Java-level deadlock: Thread-1: waiting to lock monitor 0x000000001a2e4a28 (object 0x00000000d787b4e0, a java.lang.Object), which is held by Thread-0 ...这种输出把“循环等待”关系直接画了出来。只要看到waiting to lock ... which is held by这种句式就基本断定死锁发生了。此时的处理方式一般有两种要么 kill 掉其中一个线程让另一方释放锁但会丢失该线程的任务要么通过代码修复——调整加锁顺序使所有线程都按同一全局顺序获取锁。6.2 数据库死锁排查以 SQL Server 和 MySQL 为例数据库死锁的官方错误码为 1205SQL Server和 1213MySQL。两种数据库都提供了死锁图或死锁日志但获取方式不同SQL Server使用DBCC TRACEON(1222)开启死锁跟踪之后在错误日志中查看死锁图。也可用系统视图sys.dm_tran_locks实时查看锁等待关系。MySQL InnoDB执行SHOW ENGINE INNODB STATUS\G在LATEST DETECTED DEADLOCK一节中能看到最近一次死锁的详细信息包括持有锁的事务、等待锁的事务、回滚了哪个事务。排查数据库死锁时有一个核心技巧不要盯着 SQL 本身看要盯着索引。大量数据库死锁的根源是行锁范围不一致——两个事务都更新同一张表但一个通过索引 A 扫描另一个通过索引 B 扫描行锁获取顺序不一致形成环路。解决方案通常是统一访问路径、为 WHERE 条件设计合适的联合索引或者把长事务拆短。6.3 死锁与饥饿两个容易被混为一谈的概念很多人把死锁和饥饿搞混这里顺手理清。死锁是多个进程互相等待谁也无法推进系统整体处于停滞饥饿是某个进程长时间得不到所需资源但其他进程可能正常运行。饥饿的典型例子是优先级反转低优先级进程持有资源高优先级进程不断抢占 CPU低优先级进程永远无法运行完成导致它持有资源无法释放甚至引发连锁问题。饥饿不一定需要循环等待条件也不需要“不可剥夺”条件同时满足。它更像一个“长期被冷落”的调度问题而非“卡死不动”的资源争夺问题。明白这个区别面试回答“死锁和饥饿的区别”时就能拿高分。6.4 常见误判与避坑指南我把平时答疑和排障时碰到的常见误判整理成一个速查表方便你复习和排障时对照误区实际情况正确理解不安全状态必然死锁只是找不到安全序列不代表当前一定发生死锁不安全状态是死锁的必要条件不是充分条件死锁必然发生在前一个不安全状态死锁发生后系统一定处于不安全状态但死锁前一刻系统可能还没进入“找不到安全序列”的状态安全→不安全→死锁是一条路径不是唯一路径银行家算法能解决所有死锁问题银行家算法只能做死锁避免无法处理已经发生的死锁且自身有适用前提死锁预防、避免、检测恢复是不同层次的策略死锁检测发现后必须重启进程可以中止部分进程、回滚事务、资源剥夺后再重启死锁解除策略里有多种恢复手段7. 复习与面试视角这个知识点常怎么考7.1 期末考点与简答模板如果你是学生期末复习操作系统这一章时要抓住以下常见考法概念辨析题“不安全状态是否必然导致死锁为什么”标准答法是不安全状态不一定导致死锁。不死锁的充要条件是死锁四条件互斥、持有并等待、不可剥夺、循环等待中至少一个不成立而不安全状态只代表系统找不到一个安全序列实际进程推进可能绕过危险路径因此不一定死锁。计算题给一个资源分配状态要求用银行家算法判断安全性并找安全序列。这种题务必先求出Need Max - Allocation再反复迭代检查Need[i] ≤ Work。每次找一个可满足的进程将它的Allocation加回Work标记 Finish。设计题如何破坏四个必要条件来预防死锁破坏互斥很难资源固有属性破坏持有并等待一次性请求所有资源破坏不可剥夺可剥夺调度破坏循环等待资源有序分配法。7.2 面试加分回答结合生产经验讲“为什么不安全不等同于死锁”校招面试时高频题目是“了解死锁吗如果系统进入了不安全状态你会怎么做”很多只背课本的人会答“立刻采用银行家算法回退或拒绝分配”。但带过生产环境的人更会补充一句在实际系统中我们不会对所有资源都跑银行家算法因为进程的最大需求难以预知。一般情况下优先通过调整锁顺序、减少持锁时间、使用超时机制来预防死锁一旦发生死锁直接依赖数据库或 JVM 的死锁检测机制来定位并回滚。理解“不安全状态不等于死锁”让我在排查问题时不会一看到资源吃紧就误判为死锁而是先查锁等待图再决定是否介入。这番话能体现你对理论与实践差距的理解面试官通常会眼前一亮。如果再补充一句“我们线上 MySQL 死锁日志里InnoDB 会自动回滚代价较小的事务来解除死锁所以大多数死锁根本不需要人工干预”就更贴近实战了。8. 从理论到落地我的经验与建议8.1 学习路径上的三点建议第一先啃下安全序列的计算再谈概念辨析。银行家算法的计算熟练度上去了你才能具象化地理解“为什么不安全状态下算法找不到可推进的进程”。第二在真实开发中主动翻阅死锁日志。如果你们项目里有 MySQL、Redis、Java 应用就把排查死锁的命令背下来遇到线上卡顿先导出 dump 看一眼远比背十遍理论有用。第三动手写一个多线程死锁 Demo比如两个线程互相持有对方需要的锁再配合 jstack 观察输出。自己亲手复现过一次死锁对“循环等待”的理解会质变。8.2 平时写并发代码时的习惯我在写 Java 并发代码时有一个习惯两个以上锁的加锁顺序必须全局一致。比如操作账户 A 和账户 B无论入口是转出还是转入先锁小的账户 ID再锁大的账户 ID。这个习惯能直接从源头上切断循环等待比任何检测手段都省心。同理在数据库层面多个事务更新多张表时尽量保证所有事务都按同一顺序操作表死锁率会显著下降。另外减少持锁时间也是一条铁律。不要在持锁期间做耗时操作比如网络请求、磁盘 I/O、远程调用。能放到锁外面的逻辑尽量放出去锁的粒度越小竞争窗口越窄死锁概率越低。8.3 遇到“奇怪”死锁时的排查思路如果你已经确认系统发生死锁但按常规手段查不出原因我建议按以下顺序排查先确认是不是锁等待用 jstack 或数据库状态命令看 LOCK WAIT而不是只看阻塞。再看锁顺序是否一致翻代码里所有加锁点列出全局锁序表看是否存在交叉加锁。这一步能定位绝大多数设计型死锁。查事务边界数据库死锁经常因为事务太长、跨表操作过多。把事务切短一次只做一件事。查连接池大小连接池设得太小也可能因为每个线程都持有一个数据库连接却又在等另一个连接形成连接层死锁。这类问题光看 SQL 死锁日志是看不出来的要看线程栈全貌。查缓存与索引有时候更新同一行时因为间隙锁、插入意向锁冲突会被 InnoDB 判断为死锁。此时 SQL 本身没错是索引选择导致锁范围不一致在“打架”。8.4 最后聊聊做题和排障的心态如果你正在准备考试我建议你把“不安全状态不必然死锁”当作一个哲学问题来理解安全状态是强保证不安全状态是弱警告死锁是最终恶果。这三者的关系远比“听到警报房子就塌了”要复杂得多。排障时也是一样线上出现资源紧张不要急着下“死锁”的判断先看清楚锁等待图再对症下药。多数情况下程序只是短暂地踩进了不安全区域很快自己走了出来。我个人在实际教学和排障中的体会是真正坑人的不是死锁本身而是把“可能死锁”误当成“已经死锁”后的慌乱操作。比如往数据库里手动杀会话、在 Java 应用里随性 kill 线程往往导致更大的数据不一致。先准确判断状态再决定介入方式才是操作系统这门课教给排障者最好的礼物。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →