一句话总结
死锁是两个或多个进程互相等待对方持有的资源,谁都动不了。它必须同时满足四个条件:互斥、占有且等待、不可抢占、循环等待。处理思路分四层:预防(破坏必要条件,如一次性申请全部资源)、避免(银行家算法,动态检查分配后是否仍安全)、检测与恢复(画等待图找环,杀进程或回滚)、鸵鸟算法(不管,UNIX/Linux 的选择)。
初级理解
四个必要条件
| 条件 | 含义 | 日常例子 |
|---|---|---|
| 互斥 | 资源一次只能被一个进程占用 | 打印机、锁 |
| 占有且等待 | 拿着已有资源不放,再去申请新资源 | 握着筷子等汤勺 |
| 不可抢占 | 别人不能强行抢走已分配的资源 | 锁不会被自动释放 |
| 循环等待 | 存在一个进程链,彼此等待 | A 等 B,B 等 C,C 等 A |
四个条件缺一不可。所以只要破坏其中任意一个,死锁就不会发生。
最小死锁例子
# 线程 A # 线程 B
lock(X); lock(Y);
lock(Y); ← 阻塞等 B 释放 lock(X); ← 阻塞等 A 释放
# 结果:A 拿着 X 等 Y,B 拿着 Y 等 X,永远卡死
# 对策:全局统一加锁顺序(都先 X 后 Y),循环等待条件被破坏
中级深入
预防 vs 避免
# 预防(静态策略,从条件入手,限制多但彻底):
# 1. 破坏"占有且等待":一次性申请所有资源,要么全给要么全不给
# 2. 破坏"不可抢占":申请新资源失败时,先释放已占有的再重试
# 3. 破坏"循环等待":资源编号,只允许按递增顺序申请
# 4. 破坏"互斥":让资源可共享访问(打印机用假脱机 SPOOLing)
# 避免(动态策略,不限制申请方式,但每次分配前做安全判断):
# 典型就是银行家算法:分配后系统仍处于"安全状态"才允许
银行家算法
# 数据结构(5 个进程 P0-P4,3 类资源 A/B/C):
# Max 每个进程最多需要多少
# Allocation 已分配多少
# Need = Max - Allocation 还差多少
# Available 系统还剩多少
# 安全性检查步骤:
# 1. Work = Available;Finish[i] = false
# 2. 找 Need[i] <= Work 且未完成的进程 i
# 3. 假设它跑完:Work += Allocation[i];Finish[i] = true;回第 2 步
# 4. 所有 Finish 都为 true → 安全状态;否则不安全,拒绝这次分配
# 特点:需要预知 Max(实际系统很少知道),理论价值大于工程价值
高级进阶
死锁检测
# 资源分配图:进程→资源(请求边),资源→进程(分配边)
# 图里存在环 = 死锁(每类资源只有一个实例时成立)
# 多实例资源用"等待图":只画进程之间的等待边,找环
# 检测时机:每次请求都查(开销大)/ 定时查 / CPU 利用率掉到阈值以下时查
# 检测到后的恢复手段:
# 1. 杀进程:杀掉环上全部或逐个杀(牺牲品选择有讲究)
# 2. 回滚:把进程恢复到之前的检查点,释放资源重来
# 3. 抢占:强行抢资源给别的进程(可能造成任务失败)
活锁与饥饿
# 活锁:进程没阻塞,但互相"礼让"导致谁都没进展
# 例:两个人走廊相遇,同时让同一边,反复几次还是撞上
# 解法:加随机退避(以太网冲突退避、锁竞争加 jitter)
# 饥饿:进程一直得不到资源(不是死锁,别人一直在用)
# 例:短作业优先下长作业永远排不上
# 解法: Aging(等待越久优先级越高)
实战场景
# 场景1:Java 死锁最小复现
Object x = new Object(), y = new Object();
new Thread(() -> { synchronized (x) { sleep(100); synchronized (y) {} } }).start();
new Thread(() -> { synchronized (y) { sleep(100); synchronized (x) {} } }).start();
# 定位:jstack PID 找到 "Found one Java-level deadlock" 段
# 修复:统一加锁顺序、或改用 ReentrantLock.tryLock(timeout) 带超时
# 场景2:数据库死锁
# 两个事务互相持有对方要的行锁
# InnoDB 自动检测(wait-for graph 找环),回滚代价小的事务
# 业务侧对策:事务尽量短、按固定顺序更新、加索引减少锁范围
# 排查:SHOW ENGINE INNODB STATUS\G 看 LATEST DETECTED DEADLOCK
# 场景3:分布式锁死锁
# 客户端拿到锁后宕机,锁永不释放 → 死锁
# 对策:锁带 TTL(自动过期)+ 续期(看门狗,Redisson 的实现)
# 释放锁要校验持有者(Lua 脚本比对 value),避免误删别人的锁
面试模拟
面试官:死锁产生的四个条件?破坏一个就能避免吗?
你:互斥、占有且等待、不可抢占、循环等待。四个必须同时满足才会死锁,所以破坏任意一个就能预防。工程上最常用的是破坏循环等待——规定全局一致的加锁顺序;其次是避免占有且等待——一次性申请全部资源或用 tryLock 带超时。
面试官:银行家算法在做什么?
你:它是死锁避免的代表:每次分配前模拟分配后的状态,按安全性检查流程看是否还能找到一个让所有进程顺利完成的序列,找不到就拒绝这次分配。前提是要预先知道每个进程的最大需求,所以实际系统很少用,更多是操作系统课的经典模型。
面试官:死锁、活锁、饥饿的区别?
你:死锁是进程都阻塞了,互相等待永远无法推进;活锁是进程没阻塞但一直在互相退让,同样没有进展;饥饿是单个进程长期得不到资源,别人一直在用。死锁靠检测恢复,活锁靠随机退避,饥饿靠 Aging 提优先级。