美文网首页
并发场景下死锁

并发场景下死锁

作者: Easy的幸福 | 来源:发表于2019-10-15 16:09 被阅读0次
    案例场景

    例如账户A 转账户B、账户C 转账户D这两个转账操作。

    class Account {
      private int balance;
      // 转账
      void transfer(Account target, int amt){
        // 锁定转出账户
        synchronized(this){     ①
          // 锁定转入账户
          synchronized(target){ ②
            if (this.balance > amt) {
              this.balance -= amt;
              target.balance += amt;
            }
          }
        }
      } 
    }
    

    这种方式采用了细粒度锁。使用细粒度锁可以提高并行度,是性能优化的一个重要手段。

    但是会出现一个新的问题就是死锁。

    上面转账的代码是怎么发生死锁的呢?我们假设线程T1执行账户A转账户B的操作,账户A.transfer(账户B);同时线程T2执行账户B转账户A的操作,账户B.transfer(账户A)。当T1和T2同时执行完①处的代码时,T1获得了账户A的锁(对于T1,this是账户A),而T2获得了账户B的锁(对于T2,this是账户B)。之后T1和T2在执行②处的代码时,T1试图获取账户B的锁时,发现账户B已经被锁定(被T2锁定),所以T1开始等待;T2则试图获取账户A的锁时,发现账户A已经被锁定(被T1锁定),所以T2也开始等待。于是T1和T2会无期限地等待下去,也就是我们所说的死锁了。

    如何预防死锁

    产生死锁的四个必要条件:

    • 互斥,共享资源X和Y只能被一个线程占用;
    • 占有且等待,线程T1已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源X;
    • 不可抢占,其他线程不能强行抢占线程T1占有的资源;
    • 循环等待,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待。

    其中,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的,到底如何做呢?

    • 破坏占用且等待条件

    可以一次性申请所有资源,新增一个账本管理员的概念,两个资源只能同时给一个线程。这个管理员只能是一个单例,因为只有一个管理员。

    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)
        }
      } 
    }
    
    • 破坏不可抢占条件

    核心是要能够主动释放它占有的资源,这一点synchronized是做不到的。原因是synchronized申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。

    lock可以后续补充。

    • 破坏循环等待条件
    用“等待-通知”机制优化循环等待 image

    为什么说是曾经满足过呢?因为notify()只能保证在通知时间点,条件是满足的。而被通知线程的执行时间点和通知的时间点基本上不会重合,所以当线程执行的时候,很可能条件已经不满足了(保不齐有其他线程插队)。被通知的线程要想重新执行,仍然需要获取到互斥锁(因为曾经获取的锁在调用wait()时已经释放了)

    notify()是会随机地通知等待队列中的一个线程,而notifyAll()会通知等待队列中的所有线程。

    class Allocator {
      private List<Object> als;
      // 一次性申请所有资源
      synchronized void apply(
        Object from, Object to){
        // 经典写法
        while(als.contains(from) ||
             als.contains(to)){
          try{
            wait();
          }catch(Exception e){
          }   
        } 
        als.add(from);
        als.add(to);  
      }
      // 归还资源
      synchronized void free(
        Object from, Object to){
        als.remove(from);
        als.remove(to);
        notifyAll();
      }
    }
    

    相关文章

      网友评论

          本文标题:并发场景下死锁

          本文链接:https://www.haomeiwen.com/subject/jjmsmctx.html