zoukankan      html  css  js  c++  java
  • 死锁怎么产生?死锁了怎么办?

    死锁是指一组相互竞争的线程互相等待,导致「永久阻塞的现象」。如果死锁已经产生,可以使用jstack指令查看当前线程的堆栈信息,通过分析线程的运行情况排查死锁问题。

    解决死锁最好的方案是预防死锁,产生死锁有4个必要条件,我们只需要破坏其中的任一一条都能避免产生死锁。
    死锁了怎么办?

    互斥锁,解决原子性问题这篇文章中讲到,互斥锁可以将代码串行化,避免线程切换导致的并发问题。当一把锁想要锁住多个资源的时候,要使用更大力度的锁,在互斥锁,解决原子性问题转账的例子中使用了类锁 Account.class 来锁住转账双方的账号。

    这样一来,A 转账给 B,B 转账给 C,这样两个操作都变成了串行化的。加入每秒有 1 万笔交易,而每个转账操作的操作都是串行的,性能完全不可接受。

    细粒度锁

    在转账操作中,我们使用了类锁Accont.class作为互斥锁,而Accont.class是所有 Account 对象共用的,这就导致转账操作完全串行化,无法并发。

    这里的问题就是锁的范围太大,其实我们不需要锁住所有账户,只需要锁定两个转账账户就就可以了。我们可以使用**细粒度锁,使用细粒度锁可以提高并行度,是性能优化的一个重要手段。**代码如下所示:

    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;
            }
          }
        }
      } 
    }
    

    我们给转出账户加一把锁,给转入账户也加一把锁。转账时,先尝试锁定转出账户 this,锁定成功之后再尝试锁定转入账户 target,只有当两个账户都被锁定时才会执行转账操作。这样一来,我们即保证了转账操作不会出现金额问题,也能让多个账户之间的转账操作并发执行。

    死锁问题

    死锁指的是,一组互相竞争资源的线程因互相等待,导致「永久」阻塞的现象

    以上面的 transfer() 方法为例,线程 T1 执行 A 向 B 转账的操作,线程 T2 执行 B 向 A 转账的操作。如果线程 T1 和 T2 同时执行 transfer() 会导致这样一个线程:

    • 线程 T1 持有账户 A,需要请求账户 B
    • 线程 T2 持有账户 B,需要请求账户 A

    此时就造成了死锁的局面:线程 T1 和 T2 互相持有对方需要的资源,同时也都需要对方所持有的资源。

    什么情况下会产生死锁呢?有个叫 Coffman 的牛人早就总结过了,只有以下这 4 个条件都发生时才会出现死锁:

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

    如何避免死锁

    只有在互斥、占有且等待、不可抢占、循环等待这 4 个条件都满足的条件下才会产生死锁,所以我们只要破坏其中一个条件就可以避免死锁

    描述 解决方案
    互斥 共享资源 X 和 Y 只能被一个线程占用;
    占有且等待 线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X; 一次申请所有资源
    不可抢占 其他线程不能强行抢占线程 T1 占有的资源; 主动释放线程占有的资源
    循环等待 T1 等待 T2 占有的资源,T2 等待T1 占有的资源 按序申请资源:先申请资源需要小的,再申请资源需要大的

    破坏占有且等待条件

    一次申请所有资源,就可以破坏占有且等待条件。也就是不允许账户 A 自己去加锁和解锁,也不允许账户 B 自己去加锁和解锁,而是由一个管理员管理所有账户的锁。管理员则保证一次申请所有资源,即同时拿到账户 A 和账户 B 的锁。

    对应到编程领域,“同时申请”这个操作是一个临界区,我们也需要一个角色(Java 里面的类)来管理这个临界区,我们就把这个角色定为 Allocator。它有两个重要功能,分别是:

    1. 同时申请资源 apply() 和同时释放资源 free()。账户 Account 类里面持有一个 Allocator 的单例(必须是单例,只能由一个人来分配资源)。
    2. 当账户 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)
        }
      } 
    }
    

    破坏不可抢占条件

    破坏不可抢占条件的方式是,主动释放线程占有的资源,即线程 T1 持有资源的情况下请求其他资源,如果无法获得请求的资源,则主动释放自己的占有的资源。

    synchronized主动做到主动释放资源,因为synchronized申请资源的时候,如果申请不到就会直接进入阻塞状态。进入阻塞状态的线程什么也不会做,也就不会主动释放资源。

    但是我们可以使用 java.util.concurrent 包下的 Lock 来解决这个问题。

    破坏循环等待条件

    破坏循环等待条件,可以采用对资源进行排序的方式。以转账操作为例,我们可以给每个账户分配一个id。每次转账都保证先对 id 更小的账户先进行加锁的操作。

    
    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;
            }
          }
        }
      } 
    }
    

    相关文章

    05 | 一不小心就死锁了,怎么办?
    jstack简单使用,定位死循环、线程阻塞、死锁等问题

  • 相关阅读:
    RFS入门【JS脚本应用】
    robot framework安装问题排查
    Jmeter报告模板(3种)
    【Ant】问题汇总
    上网痕迹查询助手Viewurl 2017
    Sublime3使用问题汇总
    Git常用命令
    cmp快排 结构体快排
    GCD LCM 最大公约数 最小公倍数 分数模板 (防溢出优化完成)
    三分法 模板
  • 原文地址:https://www.cnblogs.com/shuiyj/p/13185083.html
Copyright © 2011-2022 走看看