并发编程—死锁了,怎么办?

2020-12-04  本文已影响0人  瞎胡扯1

image

上一篇文章中提到了如果多个资源之间不存在关系时,尽量使用细粒度的锁,但是在实际应用中,使用细粒度的锁有时会付出惨重代价的,这个代价就是可能造成可怕的“死锁”。

那么什么是死锁呢?

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

如何预防死锁

并发程序一旦死锁,一般没有特别好的方法,很多时候我们只能重启应用。因此,解决死锁问题做好的办法还是规避死锁。

那如何避免死锁呢?要避免死锁就需要先分析死锁发生的条件,只有以下四个条件都发生时才会出现死锁:

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

反过来分析,也就是说我们只要破坏其中一个,就可以成功避免死锁的发生。

其中,互斥这个条件我们是无法破坏,因为我们使用锁的目的就是互斥。其他三个条件都有办法破坏掉,那如何做呢?

  1. 对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。
  2. 对应“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这个不可抢占这个条件就破坏掉了。
  3. 对于“循环等待”这个条件,可以靠申请资源来预防,也就是资源是有线性顺序的,申请的时候可以先申请序号小的,再申请序号大的,这样线性化后就不会存在循环了。

下面我们就具体分析一下在实际编码中如何操作

1、破坏占用且等待条件

从理论上讲,要破坏这个条件,可以一次性申请所有资源。那么如何才能一次性的获取所有的资源呢,我们可以添加一个锁管理员,也就是每次申请资源时,都向锁管理员申请,释放锁也都一起把资源归还给管理员。我们定义一个类定义为 Allocator,他有两个功能,一个是apply()申请资源和free() 释放资源。如下所示:


public class Allocator {
     private List<Object> als = new ArrayList<>();
     public 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;
         }
     }
     public synchronized void free(Object from, Object to){
         als.remove(from);
         als.remove(to);
     }
}

public class Account {
    private Allocator allocator; //必须保证单例性
    private int balance;

    // 转账
    void transfer(Account tar, int amt) {

        while (!allocator.apply(this, tar)) ;
        synchronized (this) {
            try {
                this.balance -= amt;
                tar.balance += amt;
            } finally {
                allocator.free(this, tar);
            }

        }//while
    }//transfer
}

2、破坏不可抢占条件

破坏不可抢占条件看上去很简单,核心是能够主动释放他占有的资源,这一点synchronized是做不到的。原因是synchronized申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已占有的资源。

所以在JDK1.5后的版本中的java.util.concurrent包下提供了 Lock 显示锁,可以轻松的解决这个问题。

3、破坏循环等待条件

破坏这个条件,需要对资源进行排序,然后按需申请资源。具体操作和为每个资源设置一个编号,在申请锁的时候,我们按从小到大的顺序来申请。如下所示:


public class Account {
    private int id;
    private int balance;

    // 转账
    void transfer(Account tar, int amt) {
        Account left = this;
        Account right = tar;
        if(this.id > tar.id){
            right = this;
            left = tar;
        }
        synchronized (left) {
            synchronized (right){
                this.balance -= amt;
                tar.balance += amt;
            }
        }
    }
}

总结

我们今天这一篇文章主要讲了用细粒度锁来锁定多个资源时,要注意死锁的问题。这个就需要你能把它强化为一个思维定势,遇到这种场景,马上想到可能存在死锁问题。当你知道风险之后,才有机会谈如何预防和避免,因此,识别出风险很重要

再者就是,在选择具体方案的时候,还要评估一下操作成本,从中选择一个成本最低的方案

上一篇下一篇

猜你喜欢

热点阅读