美文网首页
并发编程—死锁了,怎么办?

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

作者: 瞎胡扯1 | 来源:发表于2020-12-04 17:22 被阅读0次

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

    总结

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

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

    相关文章

      网友评论

          本文标题:并发编程—死锁了,怎么办?

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