关于多线程中的死锁

142 阅读5分钟

线程活性故障是由资源稀缺性或者程序自身的问题和缺陷导致线程一直处于非RUNNABLE状态, 或者线程虽然处于RUNNABLE状态但是其要执行的任务却一直无法进展的故障现象。

死锁

​ 是线程的一种常见活性故障。如果两个或者更多的线程因相互等待对方而被永远暂停(线程的生命周期状态为BLOCKED或者WAITING), 那么我们就称这些线程产生了死锁 (Deadlock),状态永远是非运行状态

​ 线程A在持有锁L1的情况下去申请锁L2,而线程B在持有锁L2的情况下去申请锁L1,两个线程都在等待对方先释放对方持有的另外一个锁,而释放锁的前提是先获得对方持有的锁,就会产生死锁。

死锁产生的条件与规避

必要条件(消除死锁产生的任意一个必要条件就可以规避死锁)

  1. 资源互斥 (Mutual Exclusion)/资源不共享 。涉及的资源必须是独占的, 即每个资源一次只能够被一个线程使用。

  2. 资源不可抢夺(No Preemption)。涉及的资源只能够被其持有者(线程)主动释放, 而无法被资源的持有者和申请者之外的第三方线程所抢夺(被动释放)。

  3. 占用并等待资源(Hold and Wait)/请求并保持。涉及的线程当前至少持有一个资源(资源A) 并申请其他资源(资源B) • 而这些资源(资源B)恰好被其他线程持有。

  4. 循环等待资源(Circular Wait)。涉及的线程必须在等待别的线程持有的资源, 而这些线程又反过来在等待第1个线程所持有的资源。

    ​ 一个类的一个同步方法调用该类的另外一个同步方法)并不会导致死锁, 这是因为Java中的锁(包括内部锁和显式锁)都是可重入的(Reentran几这种情形下线程再次申请这个锁是可以成功的。

规避:

  1. 锁排序法

    ​ 相关线程使用全局统一的顺序申请锁。 假设有多个线 程需要申请资源(锁) {Lock1, Lock2. …, Lock对,那么我们只需要让这些线程依照一 个全局(相对于使用这种资源的所有线程而言)统一的顺序去申请这些资源,就可以消除 “循环等待资源” 这个条件 ,从而规避死锁。

缺点: 按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁,并知道他们之间获取锁的顺序是什么样的。

2.使用ReentrantLock.tryLock(long, Time Unit)申请锁。允许我们为锁申请这个操作指定一个 超时时间。在超时时间内, 如果相应的锁申请成功, 那么该方法返回true,或者等待时间超过指定的 超时时间(此时该方法返回 false)。 因此, 使用tryLock(long, Time Unit)来申请锁可以避免一个线程无限制地等待另外一个线程持有的资源, 从而最终能够消除死锁产生的必要条件中的第三个 。

缺点:

但是由于存在锁的超时,通过设置时限并不能确定出现了死锁,每种方法总是有缺陷的。有时为了执行某个任务。某个线程花了很长的时间去执行任务,如果在其他线程看来,可能这个时间已经超过了等待的时限,可能出现了死锁。

  1. 粗锁法

    ​ 粗锁法(Coarsen-grainedLock) -使用粗粒度的锁代替多个锁。从消除“占用并等待资源”出发我们不难想到的一种方法就是,采用一个粒度较粗的锁来替代原先的多个粒度较细的锁,这样涉及的线程都只需要申请一个锁从而避免了死锁。

    ​ 粗锁法的缺点是它明显地降低了并发性并可能导致资 源浪费。

常见情况

事实上,我们接触到的能够导致死锁的代码可能并不直接具备图 7-3 所示的特征(持有一个锁并申请另外一个锁的特征),常见的情况可能是一个方法在持有一个锁的情况下调用一个外部方法 (Alien Method),而这个方法是一 个同步方法。

死锁的恢复

如果代码中使用的是内部锁或者使用的是显式锁而锁的申请是通过Lock.lock()调用实现的,那么这些锁的使用所导致的 死锁故障是不可恢复的, 而我们唯一能够做的就是重启Java虚拟机。

如果代码中使用的是显式锁且锁的申请是通过Lock.locklnterruptibly()调用实现的,那么这些锁的使用所导致 的死锁理论上是可恢复的, 但是, 死锁的恢复实际可操作性并不强一进行恢复的尝试可能是徒劳的(故障线程可无法响应中断)且有害的.

死锁的自动恢复有赖于线程的中断机制, 其基本思想是: 定义一个工作者线程 DeadlockDetector专门用于死锁检测与恢复.

死锁自动恢复的实际意义并不大,一恢复,还是会接着发生。所以主要采取避免的方式。