[ 并发编程 ] 产生死锁的 —— 四大必要条件 和 解决方案

Posted 削尖的螺丝刀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[ 并发编程 ] 产生死锁的 —— 四大必要条件 和 解决方案相关的知识,希望对你有一定的参考价值。

[ 什么是死锁? ]


      

        什么是死锁 ? 一般 对“死”字使用,在我们日常生活中都是极其避讳的,而这字偏偏又搭上一个 “锁” 字,就好像你永远也不可能解开你暗恋女神心中的那把锁一样...

        好了,意思大概就是这么个意思,死锁是让人很头疼的东西,但是女神的死锁解不开,并发编程中的锁却是有办法可解的...

        

我们来看看对死锁比较广泛的一个解释:

        

        死锁是指 两个或两个以上 的进程(或线程) 在执行过程中,由于竞争同一资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去

        此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(或线程)称为死锁进程(或线程)。

有了上面的理论基础,我们接下来直接上干货,一举击破那让人纠结又头疼的 锁 :

[ 死锁必备条件 ]


死锁必须满足的 四个条件,缺一不可

  1. 互斥 —— 共享资源只能被一个线程占用
  2. 占有且等待 —— 拿到一个锁后都要等待另一个锁
  3. 不可被抢占 —— 其他线程不可强行抢占已被某个线程锁占有的资源
  4. 循环等待 —— T1等待T2占有的资源,T2等待T1占有的资源

[ 死锁的解决方案 ]


解锁口诀:要想破除死锁只需打破上面任意一个条件即可 

1. 破坏互斥:

        注意,在加锁的场景下,互斥条件肯定无法被破坏的,互斥的前提条件就是操作了 不该共享 的资源,也就是所谓的 临界资源 (一个时间段内只允许一个操作者使用),这也是我们并发编程要达到的目的,所以严格意义上来说,破坏互斥并不是我们想要的结果,当然你场景符合的前提下也是可以用的

        如果单纯从解决死锁的角度来看,我们把不该共享的资源共享即可,手段多种:

        如用 volatile 修饰 共享资源对象 (但是这又会产生一个操作不原子的风险,介于篇幅,本文默认读者已经了解volatile机制)。 

        或者直接使得共享资源对象变成线程安全且具有原子能力的共享对象(如AtomicInteger,本质上在内部也使用同步手段解决了线程安全问题,只不过对使用者屏蔽了这块逻辑,使得我们感受不到互斥or死锁的风险),使用相关的手段可以达到破坏互斥并决绝死锁问题 ...


2. 破坏占有且等待:

        使用一个单例的公用对象来分配锁的资源,拿银行转账为例: 账户 Account 类里面持有一个 Allocator 的单例(必须是单例,只能由一个人来分配资源)。

        当账户 Account 在执行转账操作的时候,首先向 Allocator 同时申请转出账户和转入账户这两个资源,成功后再锁定这两个资源;当转账操作执行完,释放锁之后,我们需通知 Allocator 同时释放转出账户和转入账户这两个资源。具体的代码实现如下。




class Allocator 

  private List<Object> als = new ArrayList<>();

  // 一次性申请所有资源
  synchronized boolean apply(Object from, Object to) 

    if(als.contains(from) || als.contains(to))
      return false;  
     else 
      als.add(from);
      als.add(to);  
    
    return true;
  


  // 归还资源
  synchronized void free(Object from, Object to)

    als.remove(from);
    als.remove(to);
  







class Account 

  // actr应该为单例
  private Allocator actr;
  private int balance;


  // 转账
  void transfer(Account target, int amt)

    // 一次性申请转出账户和转入账户,直到成功

    while(!actr.apply(this, target))

    try

      // 锁定转出账户
      synchronized(this)     
         
        // 锁定转入账户
        synchronized(target)           
          if (this.balance > amt)
            this.balance -= amt;
            target.balance += amt;
          
        
      

     finally 

      actr.free(this, target)

    

   






3. 破坏不可被抢占:

由于 Synchrolized 本身无法主动释放锁,所以要破坏这一条则只能使用 Lock 提供的方法。


4. 破坏循环等待:

        

        造成循环等待原因是两个线程拿到锁的顺序不一,如果对拿到锁的顺序从小到大(或者从大到小)排序的话,拿到L1才能拿到L2,这样顺序取锁,那么就不会发生死锁问题了(该方案成本最低)。          

举例: 我们假设每个账户都有不同的属性 id,这个 id 可以作为排序字段,申请的时候,我们可以按照从小到大的顺序来申请。比如下面代码中,①~⑥处的代码对转出账户(this)和转入账户(target)排序,然后按照序号从小到大的顺序锁定账户。这样就不存在“循环”等待了




class Account 

  private int id;
  private int balance;

  // 转账
  void transfer(Account target, int amt)
    Account left = this        // ①
    Account right = target;    // ②
    if (this.id > target.id)  // ③
      left = target;           // ④
      right = this;            // ⑤
                              // ⑥
    // 锁定序号小的账户
    synchronized(left)
      // 锁定序号大的账户
      synchronized(right) 
        if (this.balance > amt)
          this.balance -= amt;
          target.balance += amt;
        
      
    
   




以上是关于[ 并发编程 ] 产生死锁的 —— 四大必要条件 和 解决方案的主要内容,如果未能解决你的问题,请参考以下文章

并发编程之死锁

实战并发编程 - 07循环等待&死锁问题

猿创征文 | 深入理解高并发编程 ~ 开篇

java并发编程死锁

java并发编程04:死锁

比特博文|死锁的产生防止避免检测和解除