死锁是什么,为什么会发生,怎么解决
死锁是并发控制里最经典、也最容易在面试中被单独追问的一题。它和前面学过的:
- 锁
- 信号量
- 同步与互斥
- 哲学家就餐
都是连在一起的。
最核心的一句话是:
死锁是多个线程或进程因为争夺资源而互相等待,导致谁都无法继续执行的状态。
一、什么是死锁
死锁可以理解成一种“大家都卡住了,而且谁都没法主动往前走”的状态。
最典型的例子是:
- 线程 A 先拿到锁 1,再去申请锁 2
- 线程 B 先拿到锁 2,再去申请锁 1
于是:
- A 等 B 释放锁 2
- B 等 A 释放锁 1
两边都不放手,最后谁都无法继续执行,这就是死锁。
二、死锁和普通阻塞有什么区别
这两个概念很容易混。
普通阻塞
线程只是:
在等待某个外部条件发生
例如:
- 等磁盘 IO
- 等网络数据
- 等定时器
- 等锁释放
只要这个条件将来能自然发生,线程就还能继续执行。
死锁
死锁则是:
多个线程彼此等待形成闭环,如果没有外部干预,就可能一直卡下去。
所以:
- 多个线程都阻塞,不一定是死锁
- 但死锁一定是一种特殊的“相互等待型阻塞”
三、死锁的四个必要条件
死锁之所以重要,是因为它通常要同时满足四个必要条件。
1. 互斥条件
资源一次只能被一个线程或进程占有。
例如:
- 一把锁
- 一个打印机
- 一个独占设备
如果资源本来就能共享使用,就不会在这个点上形成死锁。
2. 请求并保持条件
线程已经拿到了一部分资源,同时还继续申请其他资源,并且在没拿到新资源前不释放已有资源。
例如:
- 已经拿了锁 A
- 还想再去拿锁 B
- 拿不到 B 也不放 A
3. 不可剥夺条件
线程已经拿到的资源不能被别人强行抢走,只能由它自己释放。
锁就是最典型的这种资源。
4. 循环等待条件
多个线程形成环状等待链。
例如:
- A 等 B 的资源
- B 等 C 的资源
- C 又等 A 的资源
这会形成闭环。
四、为什么说这四个条件缺一不可
因为只要破坏其中任意一个条件,死锁就不成立。
所以很多避免死锁的方法,本质上都是在主动破坏这四个条件中的某一个。
工程里最常见的做法,不是复杂算法,而是:
优先破坏循环等待条件。
例如统一加锁顺序。
五、最常见的死锁场景
1. 多把锁获取顺序不一致
这是现实开发里最经典的场景。
例如:
线程 A
- 先锁 A
- 再锁 B
线程 B
- 先锁 B
- 再锁 A
如果时机刚好卡住,就会形成死锁。
2. 哲学家就餐问题
你前面学过的哲学家就餐,本质上也是多个线程竞争多个资源,并最终形成循环等待的经典模型。
它的核心意义,不是题目本身,而是说明:
资源申请顺序不合理,就很容易形成死锁。
3. 数据库事务互相等待
在数据库里,不同事务彼此持有对方想要的锁,也会形成死锁。
所以死锁不是纯理论,在工程里也经常真实发生。
六、怎么处理死锁
教材上通常把死锁处理分成四类:
1. 预防(Prevention)
思路是:
提前破坏死锁四个必要条件之一。
例如:
- 统一锁顺序(破坏循环等待)
- 一次性申请所有资源(破坏请求并保持)
- 某些场景允许资源可抢占(破坏不可剥夺)
2. 避免(Avoidance)
思路是:
在分配资源前,先判断这次分配会不会让系统进入危险状态。
最经典的代表就是:
- 银行家算法
不过它更偏理论,在现实工程里不常直接使用。
3. 检测(Detection)
思路是:
先允许死锁发生,再靠系统去检查有没有等待环。
数据库系统里这种思路比较常见。
4. 解除(Recovery)
如果死锁已经发生,就必须强行打破。
常见方式包括:
- 杀掉某个进程 / 线程
- 回滚某个事务
- 强制释放资源
这通常代价比较大,所以工程上一般更希望“提前预防”,而不是“事后解除”。
七、工程里最常见的防死锁办法
现实开发里最实用的,不是银行家算法,而是一些规约和经验。
1. 统一加锁顺序
这是最重要的一条。
如果所有线程都规定:
- 先锁 A
- 再锁 B
- 再锁 C
那就很难形成循环等待。
2. 一次性申请所有资源
如果做得到,就尽量避免“拿一半资源再去等另一半”。
3. 减少锁嵌套
锁嵌套越深,组合越复杂,死锁概率越高。
4. 缩小临界区
锁住的代码越少,等待链就越短,出问题概率也会降低。
5. 使用超时机制
不要无限等待锁。超时后回滚、重试或报错,都比无限卡住要好。
6. 使用更高层并发工具
比如:
- 线程安全容器
- 任务队列
- Actor 模型
- 无锁结构
能减少手写复杂锁组合的机会。
7. 申请新资源失败时,先释放已占资源再重试
这类思路本质上是在尽量破坏“请求并保持”或“不可剥夺”。
也就是:
- 线程已经拿着一部分资源
- 继续申请新资源时如果失败
- 就先释放已占有的资源
- 稍后再重新申请
它的重点是避免线程长期拿着部分资源不放,从而形成等待链。
8. 尽量让资源可共享
这类思路本质上是在尽量削弱“互斥”。
例如:
- 只读资源共享访问
- 读写分离
- 假脱机(Spooling)
如果资源本身不需要严格独占,那么死锁成立的前提也会跟着变弱。
八、操作系统里常见的死锁类型
从表现上看,死锁常见可以分成两类:
1. 资源死锁
这是最典型的一类。
也就是多个线程或进程彼此占有一部分资源,同时又在等待别人的资源释放。
例如:
- A 占有资源 1,等待资源 2
- B 占有资源 2,等待资源 1
2. 通信死锁
这类死锁不一定卡在“抢资源”上,而是卡在“等消息、等事件、等通知”上。
例如:
- A 等 B 发消息
- B 等 C 发消息
- C 又等 A 发消息
结果大家都在等,谁也推进不了。
九、死锁、饥饿、活锁的区别顺手补充
1. 死锁
大家都在等彼此,彻底卡住。
2. 饥饿
某个线程长期拿不到资源,但系统整体仍然在运行。
3. 活锁
线程都在动,但谁都没有真正推进工作。
例如两边都不断让步,但谁都不真正进入临界区。
所以:
- 死锁是“彻底卡死”
- 饥饿是“某个人一直没机会”
- 活锁是“大家很忙,但没有结果”
十、总结
死锁的本质是多个线程或进程在资源竞争中形成相互等待的闭环,最终谁都无法继续执行。它和普通阻塞的区别在于,普通阻塞等待的条件通常会自然发生,而死锁如果没有外力干预就可能一直持续下去。死锁发生通常需要同时满足四个必要条件:互斥、请求并保持、不可剥夺和循环等待。工程上最常见也最有效的预防思路,是统一资源申请顺序,尽量破坏循环等待条件。
