你真的理解什么是死锁吗?

一. 死锁的概念

两个或多个进程,由于资源的竞争或者彼此间的通信而造成的阻塞现象,如果没有外力干预,它们将无法进行下去,这就发生了死锁。

更规范的定义:集合中的每一个进程都在等待只能由本集合中的其他进程才能引发的事件(资源),那么该组进程是死锁的。

上面说到的竞争的资源可以是一切能称为资源的东西,比如锁,网络连接,通知事件,磁盘等

举个例子

比方说有两个线程,a 和 b

  • a 线程持有锁 a,等待锁 b
  • b 线程持有锁 b,等待锁 a

这样这两个线程就出现了死锁

二. 产生死锁的必要条件

  • 互斥条件:一个资源一次只能被一个进程访问;一旦分配给某个进程,其他进程就不能再访问(因为竞争资源的被占用是发生死锁的一个重要原因)
  • 请求和保持条件:进程在等待其他线程占用的资源(请求),与此同时,进程会一直占用着自己已经获得的资源(保持)
  • 不可剥夺条件:进程对于已经申请到的资源在使用完成之前不可以被剥夺(也就是不会被其他进程抢走自己的资源)
  • 环路等待条件:发生死锁的进程组中,每一个进程都会占有另一个进程所需要的资源,这个占有关系可以形成一个等待环路(也就是竞争资源被相互占用,无法找到突破口,只能被死死堵着)

三. 如何预防和避免死锁

1. 以特定的顺序获取资源

以上面的例子来说,A 线程先尝试获取 a 锁再尝试获取 b 锁;而 B 线程则相反。这样子的设计就很可能出现死锁。

如果一组线程都按照同样的顺序来尝试获得锁,那么就可以避免死锁的发生了。

2. 超时放弃

设置占有资源或者占有锁的最大时间,如果超过该时间仍未释放,那么就主动释放该资源

3. 预先分配资源

在进程运行之前一次性的将其所需(申请)的资源分配给他,保证其可以正常的执行完毕,不需要在进程运行时再申请资源。

该方法的弊端体现在资源利用率低的方面,因为并不是所有的资源在进程运行时都会被长时间占用,但是这样在运行前一次性分配的方式会造成所有资源在该进程的生命周期内都是不可以被释放的。

4. 银行家算法

银行家算法是一种直接避免死锁的方式,主要的思想就是动态检查进程对资源的申请,以检查是否会造成死锁。

我们通过列出各个线程(进程)的当前占有的资源,最大所需要的资源以及当前剩余的可利用资源,可以计算可得知当前是否为安全状态(也就是一定不会发生死锁),简单来说就是确保当前可以利用的资源是充足的

四. 检测死锁

如果我们不去主动预防或者避免死锁,那么我们可以通过及时检测当前是否出现死锁的方式来处理死锁问题,比如使用一些死锁检测算法

而什么时候去进行死锁检测又取决于死锁发生的频率以及一般情况下死锁涉及的进程数;这样,我们既可以选择定时检测,也可以在发现资源利用率下降时进行检测,总之,你得知道发生死锁时,你的程序会有怎样的变化。

监控工具 JConsole

JDK 也自带了一个监控工具 JConsole,在JDK/bin目录下可以找到。

连接本地进程
查询是否存在死锁

五. 解除死锁

当发生了死锁之后,我们肯定要去解决掉它的。最直接的方法肯定是重启,当然很多时候这样做不太可取。

  • 终止相关进程:检测出与死锁有关的进程,撤销或者挂起它,强制它释放资源
  • 剥夺资源:将部分被死锁进程占用的资源剥夺出来,解除死锁
  • 进程回退:将死锁进程回退到未出问题之前,不过实现难度较大。