【Java多线程】死锁的原因、必要条件、检测、预防、解除

目录

1 前言

2 死锁产生的原因

2.1 竞争资源引起进程死锁 

2.2 进程推进顺序不当引起死锁 

3 死锁产生的必要条件

4 处理死锁的基本方法

5 死锁的预防方法

5.1 摒弃“请求和保持”条件 

5.2 摒弃“不剥夺”条件 

5.3 摒弃“环路等待”条件 

5.4 系统安全状态与银行家算法

6 死锁的检测

7 死锁的解除


1 前言

所谓死锁(Deadlock),是指多个进程在运行过程中因争夺资源而造成的一种僵局(DeadlyEmbrace),当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进,同时它们所占用的cpu、内存等资源也得不到释放。

2 死锁产生的原因

2.1 竞争资源引起进程死锁 

非剥夺性资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。

在系统中所配置的非剥夺性资源,由于它们的数量不能满足诸进程运行的需要,会使进程在运行过程中,因争夺这些资源而陷入僵局。例如,系统中只有一台打印机 R1和一台磁带机 R2,可供进程 P1和 P2共享。当执行打印操作时,进程只有同获取到R1和R2资源才能打印成功,否则会一直阻塞直到获取R1、R2成功。假定 P1已占用了打印机 R1,P2已占用了磁带机 R2。此时,若 P2继续要求打印机,P2将阻塞;P1若又要求磁带机,P1也将阻塞。于是,在 P1与P2之间便形成了僵局, 两个进程都在等待对方释放出自己所需的资源。 但它们又都因不能继续获得自己所需的资源而不能继续推进, 从而也不能释放出自己已占有的资源,以致进入死锁状态。

2.2 进程推进顺序不当引起死锁 

典型的,若有两个独占资源如锁L1、L2,还有进程P1、P2,进程P1和P2都对L1和L2发起请求,若请求锁的顺序如下则会发生死锁:

P1获取锁L1
P2获取锁L2
P1请求锁L2    // P1阻塞
P2请求锁L1    // P2阻塞

3 死锁产生的必要条件

虽然进程在运行过程中可能发生死锁,但死锁的发生也必须具备一定的条件。综上所述不难看出,死锁的发生必须具备下列四个必要条件。 

  1. 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求该资源,则请求者只能等待,直至占有该资源的进程用毕释放。 
  2.  请求和保持条件:指进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。 
  3. 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。 
  4. 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0, P1, P2, …, Pn}中的 P0正在等待一个 P1占用的资源; P1正在等待 P2占用的资源, ……,Pn正在等待已被 P0占用的资源。

4 处理死锁的基本方法

为保证系统中诸进程的正常运行,应事先采取必要的措施,来预防发生死锁。在系统中已经出现死锁后,则应及时检测到死锁的发生,并采取适当措施来解除死锁。目前,处理死锁的方法可归结为以下四种: 

(1) 预防死锁。这是一种较简单和直观的事先预防的方法。该方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来预防发生死锁。预防死锁是一种较易实现的方法,已被广泛使用。但由于所施加的限制条件往往太严格,因而可能会导致系统资源利用率和系统吞吐量降低。 

(2) 避免死锁。该方法同样是属于事先预防的策略,但它并不须事先采取各种限制措施去破坏产生死锁的四个必要条件,而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。这种方法只需事先施加较弱的限制条件,便可获得较高的资源利用率及系统吞吐量,但在实现上有一定的难度。目前在较完善的系统中常用此方法来避免发生死锁,如Java中采用synchronized关键字来实现线程安全机制。

(3) 检测死锁。这种方法并不须事先采取任何限制性措施,也不必检查系统是否已经进入不安全区,而是允许系统在运行过程中发生死锁。但可通过系统所设置的检测机构,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源; 然后,采取适当措施,从系统中将已发生的死锁清除掉。

(4) 解除死锁。这是与检测死锁相配套的一种措施。当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来。常用的实施方法是撤消或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行。死锁的检测和解除措施有可能使系统获得较好的资源利用率和吞吐量,但在实现上难度也最大。 

5 死锁的预防方法

预防死锁的方法是使四个必要条件中的第 2、3、4 个条件之一不能成立,来避免发生死锁。至于必要条件 1,因为它是由设备的固有特性所决定的,不仅不能改变,还应加以 保证。 

5.1 摒弃“请求和保持”条件 

要求进程要么一次性申请到所有资源去执行,要么不持有独占资源地进入阻塞状态。以2.1中场景为例,若系统有足够的资源分配给某进程,便可把其需要的所有资源分配给该进程,这样,该进程在整个运行期间便不会再提出资源要求,从而摒弃了请求条件。在分配资源时,只要有一种资源不能满足某进程的要求,即使其它所需的各资源都空闲,也不分配给该进程,而让该进程等待。由于在该进程的等待期间,它并未占有任何资源,因而也摒弃了保持条件。

这种预防死锁的方法其优点是简单、易于实现且很安全。但其缺点却也极其明显:

  1. 可能造成资源的浪费。比如进程获取到了打印机和磁带机,但打印机只需执行1s,而磁带机要执行10min,这样任务才能完成,那在执行磁带机期间进程依然独占打印机资源,若其它进程只需要使用打印机,则会造成打印机资源的浪费。
  2. 会使进程延迟运行。因为仅当进程在获得了其所需的全部资源后,才能开始运行,但可能因有些资源已长期被其它进程占用而致使等待该资源的进程迟迟不能运行。 

5.2 摒弃“不剥夺”条件 

在采用这种方法时系统规定,进程是逐个地提出对资源的要求的。当一个已经保持了某些资源的进程,再提出新的资源请求而不能立即得到满足时,必须释放它已经保持了的所有资源,待以后需要时再重新申请,这意味着进程不能持有资源进入阻塞或等待状态。这意味着某一进程已经占有的资源,在运行过程中会被暂时地释放掉,也可认为是被剥夺了,从而摒弃了“不剥夺”条件。 

比如Java中,在synchronized代码块中使用wait()使线程等待的同时也会释放锁,若等待不释放锁的话,该锁永远得不到释放,尝试获取该锁的线程也会永远等待。

这种预防死锁的方法实现起来比较复杂且要付出很大的代价,因为:

  1. 被迫释放资源可能造成释放进程前段工作失效。一个资源在使用一段时间后,它的被迫释放可能会造成前段工作的失效,即使是采取了某些防范措施,也还会使进程前后两次运行的信息不连续。如进程P1使用打印机打印一段信息后,中途因为请求其他资源而释放了打印机,此时打印机又被进程P2获取输出了另一段信息,后面P1再获取时因为P2在前面已经打印了信息,所以信息不连续了。
  2. 种策略还可能因为反复地申请和释放资源,致使进程的执行被无限地推迟,这不仅延长了进程的周转时间,而且也增加了系统开销,降低了系统吞吐量。 

5.3 摒弃“环路等待”条件 

系统将所有资源按类型进行线性排队,并赋予不同的序号。例如,令输入机的序号为 1,打印机的序号为 2,磁带机为 3,磁盘为 4。所有进程对资源的请求必须严格按照资源序号递增的次序提出,这样,在所形成的资源分配图中,不可能再出现环路,因而摒弃了“环路等待”条件。

例如,在2.1场景中,打印机序号为1,磁带机序号为2,进程必须先申请打印机再申请磁带机。这样若P1获取了打印机,P2再获取打印机会阻塞,不会出现P1获取了打印机,P2获取了磁带机的情况。

这种预防死锁的策略与前两种策略比较,其资源利用率和系统吞吐量都有较明显的改善。但也存在下述严重问题: 

  1. 为系统中各类资源所分配(确定)的序号必须相对稳定, 这就限制了新类型设备的增加。
  2. 可能造成对资源的浪费。例如,某进程先用磁带机,后用打印机,但按系统规定,该进程应先申请打印机而后申请磁带机,致使先获得的打印机被长时间闲置。 
  3. 这种按规定次序申请的方法,会限制用户简单、自主地编程。

5.4 系统安全状态与银行家算法

在多进程同时请求公共资源的前提下,安全状态指系统一定不会进入死锁状态,不安全状态指系统可能进入死锁状态(不是一定)。

最有代表性的避免死锁的算法,是 Dijkstra 的银行家算法。为实现银行家算法,系统中必须设置若干数据结构。 银行家算法参考:https://blog.csdn.net/qq_34039868/article/details/105349138

6 死锁的检测

死锁检测有如下步骤,

(1) 可利用资源向量 Available,它表示了 m 类资源中每一类资源的可用数目。 
(2) 把不占用资源的进程(向量 Allocationi:=0)记入 L 表中,即 Li∪L。 
(3) 从进程集合中找到一个 Requesti≤Work 的进程,做如下处理,直到所有进程都被加入到L表中: 
      ① 将其资源分配图简化,释放出资源,增加工作向量 Work:=Work + Allocation i。 
      ② 将它记入 L 表中。 
(4) 若不能把所有进程都记入 L 表中, 便表明系统状态 S 的资源分配图是不可完全简化
的。因此,该系统状态将发生死锁。 

伪代码如下:

Work:=Available; 
   L:={Li |Allocation i=0∩Request i=0} 
   for all Li ∉L  do 
     begin 
       for all Request i≤Work do 
         begin 
          Work :=Work + Allocation i; 
          Li∪L; 
         end 
       end 
     deadlock :=┓(L={p1,p2,…,pn}); 

7 死锁的解除

当发现有进程死锁时,便应立即把它们从死锁状态中解脱出来。常采用解除死锁的两种方法是: 
(1) 剥夺资源。从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态。 
(2) 撤消进程。最简单的撤消进程的方法是使全部死锁进程都夭折掉;稍微温和一点的方法是按照某种顺序逐个地撤消进程,直至有足够的资源可用,使死锁状态消除为止。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章