Skip to content
Go back

死锁的四个条件与银行家算法

死锁:四个条件缺一不可,破坏一个就能免疫

一句话结论(30s)

死锁是四个必要条件同时满足的必然结果,因为互斥、持有并等待、不可剥夺、环路等待缺一不可,所以工程上只需破坏任意一个(最常用破坏环路等待),就能让死锁无从发生。

核心原理(2min)

四个 Coffman 条件 + 各自的破解法:破坏持有并等待用一次性申请 / tryLock,破坏不可剥夺用 tryLock 超时,破坏环路等待用资源有序分配(最常用)。银行家算法在分配前预判安全序列,存在安全序列则必不死锁;InnoDB 用等待图检测环并回滚最小代价事务。

底层深入(5-10min)

死锁的四个必要条件(Coffman 条件)

死锁不是偶然发生的——它是四个条件同时满足的必然结果:

  1. 互斥(Mutual Exclusion):资源不能被共享,同一时间只能被一个线程持有
  2. 持有并等待(Hold and Wait):线程已经持有至少一个资源,同时在等待获取其他线程持有的资源
  3. 不可剥夺(No Preemption):资源不能被强制剥夺,只能由持有者主动释放
  4. 环路等待(Circular Wait):存在线程集合 {T0, T1, …, Tn},T0 等 T1 持有的资源,T1 等 T2…,Tn 等 T0

这四个条件是死锁的充要条件——破坏任何一个,死锁就不可能发生。

想一想:为什么说”破坏一个就能免疫”?因为四个条件是”与”的关系——必须同时满足才死锁;破坏任意一个,整套条件就不成立,死锁自然无从发生。

如何破坏这四个条件

破坏”互斥”

很难。大多数锁本身就是互斥的,除非用无锁数据结构(CAS/原子变量)或读写锁(读可共享)。

破坏”持有并等待”

一次申请所有需要的资源,没拿到就释放已持有的重新等:

// 不好:分步加锁
lock1.lock();  // 获取锁1
lock2.lock();  // 获取锁2 — 此时持有锁1等锁2,可能死锁

// 好:一次申请+tryLock
while (true) {
    if (lock1.tryLock() && lock2.tryLock()) break;
    if (lock1.isHeldByCurrentThread()) lock1.unlock();
    if (lock2.isHeldByCurrentThread()) lock2.unlock();
    Thread.sleep(randomBackoff());
}

破坏”不可剥夺”

ReentrantLock.tryLock(timeout, unit) 可以在超时后放弃等待并释放已持有锁。

想一想:为什么分步加锁会死锁、tryLock 一次申请就不会?因为 T1 拿锁1 等锁2、T2 拿锁2 等锁1 就同时满足了”持有并等待”+“环路等待”;tryLock 拿不到就全部释放重来,破坏了”持有并等待”。

破坏”环路等待”(最常用)

资源有序分配法——所有线程按统一顺序申请资源:

// 约定:永远先锁A,再锁B
// 线程1: lockA → lockB ✓
// 线程2: lockA → lockB ✓
// 永远不会形成 A→B 和 B→A 的环

数据库中也一样:所有事务按主键顺序加行锁,有效规避死锁。

想一想:为什么”永远先 A 后 B”能破坏环路等待?因为环路需要 A→B 和 B→A 两条相反方向的等待,统一加锁顺序后最多只剩 A→B 一个方向,环就无法闭合。

银行家算法:提前预判安全性

银行家算法在分配资源之前模拟判断:如果把这个资源分配给这个进程,系统是否仍存在一个”安全序列”让所有进程都能完成。有则分配,无则拒绝。

// 银行家算法核心(伪码)
bool isSafe(int process, int request[]) {
    // 假设分配
    Available -= request;
    Allocation[process] += request;
    Need[process] -= request;
    
    // 寻找安全序列
    bool finish[N] = {false};
    int work = Available;
    for (int i = 0; i < N; i++) {
        if (!finish[i] && Need[i] <= work) {
            work += Allocation[i];
            finish[i] = true;
            i = -1;  // 重新扫描
        }
    }
    
    return all(finish);  // 如果所有进程都能完成 = 安全
}

存在安全序列 → 系统处于安全状态 → 一定不会死锁。但反之不成立:不处于安全状态不一定会死锁,银行家算法是最保守的策略。

想一想:为什么”不安全状态”不一定死锁?因为银行家算法是保守的——它只在”存在安全序列”时才分配,但系统可能还有别的调度方式能完成,只是算法不愿赌;所以不安全状态是死锁的必要非充分条件。

工程实践:MySQL InnoDB 的死锁检测

InnoDB 维护一个等待图(Wait-for Graph):节点是事务,有向边表示”事务 A 等待事务 B 持有的锁”。后台线程定期扫描等待图,检测到环 → 选择回滚”撤销代价最小”的事务(undo log 最少的)。

SHOW ENGINE INNODB STATUS\GLATEST DETECTED DEADLOCK 部分详细记录了死锁涉及的事务、持有和等待的锁。

总结

策略做法适用场景
资源有序分配约定锁顺序通用,最简单有效
tryLock 超时放弃等待释放已持有无法统一顺序时兜底
银行家算法分配前预判安全序列OS 资源调度、连接池管理
死锁检测扫描等待图,回滚最小代价事务数据库(InnoDB)

章末提问

追问 1:死锁的四个必要条件是什么?为什么说”破坏一个就能免疫”?

回答思路:结论先行——互斥、持有并等待、不可剥夺、环路等待,四个是”与”的关系、同时满足才死锁,所以破坏任意一个即可。因为缺了任何一个条件,剩余三个都无法构成死锁:比如破坏环路等待(资源有序分配),线程就不会互相等成环。

追问 2:工程上最常用的破死锁手段是什么?怎么落地?

回答思路:结论先行——破坏”环路等待”的资源有序分配法,约定所有线程按统一顺序加锁。因为环路需要 A→B 和 B→A 两条相反方向的等待,统一”永远先锁 A 再锁 B”后只剩一个方向、环无法闭合;数据库里也按主键顺序加行锁,这是最简单有效的通用方案。

追问 3:银行家算法和 InnoDB 的死锁检测有什么本质不同?

回答思路:结论先行——银行家算法是”事前预防”,分配前预判是否存在安全序列;InnoDB 是”事后检测”,用等待图找环再回滚。因为银行家算法在分配资源前模拟判断”分配后是否仍存在让所有进程完成的安全序列”,有才分配、无则拒绝,代价是最保守、吞吐受限;InnoDB 则后台扫描等待图、检测到环就回滚撤销代价最小(undo log 最少)的事务。


Share this post on:

Previous Post
用户态和内核态:CPU权限隔离的底层原理
Next Post
操作系统内存不足处理:从kswapd到OOM Killer的完整链条