死锁
简单来说就是 : 线程获取了锁, 由于某些原因没有及时释放, 导致其他线程无法获取到, 且没法继续往下执行, 一直处于这种状态。
严谨来说就是: 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。(来自百科)
产生死锁的情况
产生死锁通常有三种情况
1.只有一把锁
如果这把锁是一个不可重入锁, 而又进行多次加锁, 例如下面这种情况 (代码仅供理解)
public class Tmp {
public static void main(String[] args) {
Lock lockA = new Lock(); // 假设这个是不可重入锁
lockA.lock();-----------------------------|
lockA.lock();--------------| |
... 第二层锁 第一层锁
lockA.unLock();------------| |
lockA.unLock();---------------------------|
}
}
我们记这个不可重入锁为 锁A, 如上, 某线程执行上面代码, 获取完第一次锁后, 开始尝试获取第二层锁, 但是 锁A 已经被该线程获取了, 所以第二层锁无法获取, 就会进入阻塞, 等待线程释放 锁A, 而 锁A还在它身上, 其他线程也无法获取这个 锁A, 也没法主动释放锁。就陷入了死锁
2.两个锁的情况
比如这种情况, 两个锁都已经拥有自己的锁了, 但是它们又都想去获取彼此的锁, 都在等对方释放, 但是两人都不主动释放
3.多个锁的情况
这个最经典的就是哲学家问题了
问题情形: 有张桌子上有面条和5根筷子(锁), 拥有两根筷子就可以吃面, 桌子周围还有五位哲学家(线程), 要干的事就是思考人生(线程阻塞 / 休眠) 和 吃 面条(运行), 哲学家只能拿自己左右两边的筷子
上述情形, 可以认为一根筷子就是一根锁, 而需要拿两根筷子(锁) 才能吃面条(在 CPU 上工作)
如果在某一时刻, 所有哲学家都想刚好想吃面, 如果所有哲学家都同时拿起了自己左手边的筷子
而当哲学家想拿另一个筷子的时候, 就发现没有一个哲学家能吃到面, 如果他们非常固执, 就是不放筷子, 那么五个哲学家都没法吃到面, 也就是说五个线程都没法正常工作, 也就陷入了死锁问题。
解决方案
在这种场景下, 如果要避免死锁问题的发生, 我们可以指定一个规则, 并让所有线程共同遵守
给所有的锁进行编号, 规定只能按照固定的顺序获取锁
还是上面的场景, 我们如果按照这个规则, 对所有筷子(锁) 进行编号
然后规定: 所有哲学家在选择可以获取的锁时, 必须选择编号更小的锁, 那么我们先让五个哲学家都选择一个筷子(锁)
A哲学家 在1, 2号筷子中选, 只能选小的, 那就是1号筷子, 然后 B哲学家 在2, 3筷子中选…一直到 E哲学家, 只能在1, 5号筷子中选, 只能选小的, 即1号筷子, 但是1号筷子已经被占用了, 那就只能阻塞等待, E哲学家一个筷子都没法拿
但是A, B, C哲学家也没法拿另一个筷子, 也没法吃面。唯有 D 哲学家可以拿到 5 号筷子, 然后吃面, 吃碗面, 放下筷子, 给其他哲学家吃, 这样哲学家就都能吃到面条了。
可以发现: 上面几个发生死锁的场景都有一个共同点 → 循环等待, 线程1 等待 线程 2, 线程2 又在等待其他线程…这也是发生死锁的必要条件.
总结一下, 我们可以制定一个规则, 让所有线程都去遵守——当线程可以选择获取多个锁的时候, 获取锁的顺序需要是从小到大来获取。如果小的锁还没法获取, 那就进入阻塞