什么是死锁
先介绍独占性的资源 : 即再同一时刻某种资源只能由一个进程使用, 比如打印机, 同一时刻一台打印机不能有两个输出结果
那么两个进程独占性的访问某个资源, 从而等待另一个资源的执行结果, 会导致两个进程都阻塞, 并且两个进程都不会释放各自的资源, 这种情况就是 “死锁”
死锁的典型情况
- 在一个线程中, 对同一个对象连续加锁多次, 如果是不可重入锁, 就会产生死锁问题
- 两个线程两把锁, 线程 t1 和 t2 各自针对 对象 A 和 B 加锁, 再尝试获取对方已经加锁的对象
- 多个线程多把锁 (典型实例 : 哲学家就餐问题)
可重入不可重入
在一个线程中, 在已经给一个对象上锁(还未解锁的情况下), 再次尝试给该对象加锁
可重入锁(例如 synchronized) , 会在锁内记录是哪个线程拥有我, 如果该线程再次尝试给我加锁, 就允许 (实际上只加了一次锁, 后续的再次加锁只是允许你使用该对象, 不是再多加一次锁)
不可重入, 在一个线程内, 如果针对一个对象多次加锁, 由于第一次加锁过后没有解锁, 所以后续的加锁操作就会进入锁竞争, 前面加的锁不释放, 后面在等前面的锁释放, 就会造成死锁情况
死锁的四个必要条件
- 互斥使用 (锁的基本特征): 线程 t1 拿到了锁, 线程 t2 就只能等待
- 不可抢占: 线程 t1那到了锁, 必须是 t1 主动释放锁, 不能是因为别的线程需要使用的时候, 强行抢夺锁
- 请求和保持: 线程 t1 拿到 锁A 之后, 再尝试获取 锁B, 此时 t1 仍持有 锁A, 不会因为获取 锁B 就把 锁A 给释放了
- 循环等待: 线程 t1 持有锁A, 线程 t2 持有 锁B, 在此基础上 线程 t1尝试获取 锁B, 并且线程 t2 尝试获取 锁A, 二者都不释放自己拥有的锁, 又尝试获取彼此拥有的锁
如何破除死锁
破除死锁其实就是破除上述的四个基本特征 (任意一个条件打破, 死锁就被破除)
对于 synchronized
互斥使用 / 不可抢占 / 请求和保持 都是 synchronized 的基本特征
因此我们只能从 “循环等待” (也就是代码逻辑)下手
最常用的一种死锁阻止策略就是 “锁排序”. 假设有 N 个线程尝试获取 M 把锁, 可以针对 M 把锁进行编号 (1, 2, 3, … , M ), N 个线程尝试获取锁的时候, 都按照编号, 由小到大的顺序来获取锁, 就可以避免环路等待