死锁

两个或多个进程互相等待对方持有的资源,谁都不肯让、谁也走不动,系统就像被按了暂停键——这就是死锁。它没有报错、只在特定时序下出现,是最隐蔽也最难排查的一类并发问题。

死锁的经典场景

经典例子是"哲学家就餐":五位哲学家围坐,每人需要左右两根筷子才能吃饭。若大家同时拿起左手边的筷子,就会集体等待右边的筷子,谁也无法开餐。

用两个进程更直白地描述:

  • 进程 A 持有打印机,等待扫描仪;
  • 进程 B 持有扫描仪,等待打印机;
  • A 等 B 释放、B 等 A 释放 → 永远等下去。

死锁的四个必要条件

四个条件同时满足才可能死锁,缺一不可:

  1. 互斥:资源同一时刻只能被一个进程使用(如打印机不可共享)。
  2. 占有且等待:已握着一个资源,又在等别人手里的资源。
  3. 不可剥夺:资源不能被强行抢走,只能由持有者主动释放。
  4. 循环等待:存在 A 等 B、B 等 C、C 等 A 的等待环。

其中循环等待是表象,前三者是根源——面试时最好按这个逻辑展开。

处理策略一:预防(破坏必要条件)

既然四个条件缺一不可,破坏其一即可预防:

  • 破坏"占有且等待":要求进程一次性申请全部所需资源,否则不分配。
  • 破坏"不可剥夺":允许系统强行回收已分配的资源。
  • 破坏"循环等待":给资源编号,要求所有进程按同一顺序申请资源。

工程中最常用、最有效的是最后一条——全局统一的加锁顺序:

import threading

lock_a, lock_b = threading.Lock(), threading.Lock()

def work(name):
    # 所有线程都按“先 A 后 B”的顺序加锁 → 不可能形成循环等待
    with lock_a:
        with lock_b:
            print(name, "完成")

t1 = threading.Thread(target=work, args=("任务1",))
t2 = threading.Thread(target=work, args=("任务2",))
t1.start(); t2.start(); t1.join(); t2.join()
# 输出:任务1 完成 / 任务2 完成(顺序不定,但绝不会死锁)

若某个线程反过来"先 B 后 A",两个线程就可能在 A、B 上交叉等待,形成死锁。

处理策略二:避免与检测恢复

避免(银行家算法):分配前先判断"这次分配后系统是否仍处于安全状态",像银行放贷一样,只有确认所有客户最终都能还清(完成)才批准。开销较大,实际系统用得不多。

检测与恢复:允许死锁发生,通过资源分配图周期性检测;一旦发现环路,就重启相关进程或强制回收资源。更常见的是鸵鸟算法——假装没看见,重启机器解决,多数操作系统与业务系统实际上就是这么干的。

面试速答清单

  1. 死锁的定义与四个必要条件——见上文。
  2. 如何避免死锁?——按统一顺序加锁、一次性申请、超时重试、使用 tryLock。
  3. 如何排查已发生的死锁?——Java 用 jstack 看线程栈,数据库用 SHOW ENGINE INNODB STATUS 看锁等待环。

小结:死锁 = 互斥 + 占有且等待 + 不可剥夺 + 循环等待同时成立。面试先背四条件,再说"工程上靠统一加锁顺序预防、靠超时与工具检测恢复",这道题就稳了。

笔记加载中…