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

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

作者: 瞎胡扯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;
            }
        }
    }
}

总结

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

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

相关文章

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

    [http://yby.ink/wp-content/uploads/2020/12/timg1.jpg] 上一篇...

  • Java高并发 -- 并发扩展

    Java高并发 -- 并发扩展 主要是学习慕课网实战视频《Java并发编程入门与高并发面试》的笔记 死锁 死锁是指...

  • 并发编程艺术-1

    本篇文章主要简单地介绍了并发编程的目的,上下文切换带来的影响,以及死锁的检测,解决,常见的并发资源限制。 并发编程...

  • Java Concurrent 死锁

    前言 死锁是一个比较大的概念,在并发场景下的加锁行为都有可能产生死锁问题。在Java 并发编程中会有死锁,操作系统...

  • 并发编程-死锁

    一、细粒度锁 现实世界里,账户转账操作是支持并发的,而且绝对是真正的并行,银行所有的窗口都可以做转账操作。只要我们...

  • Java 并发编程(1): Java 内存模型(JMM)

    1. 并发编程 1.1 并发编程的挑战 并发编程的目的是为了加快程序的运行速度, 但受限于上下文切换和死锁等问题,...

  • 死锁

    一、什么是死锁 并发编程的本质是将串行执行的代码编程并行执行。并发编程的目的是为了加快程序的运行速度,但是...

  • 并发编程情况下几个相应问题简介

    1.并发编程的挑战之死锁 ​ 死锁是两个或更多线程阻塞着等待其它处于死锁状态的线程所持有的锁。死锁通常发生在多...

  • 大厂敲门砖,Github霸榜的顶级并发编程宝典被我搞到手了!

    并发编程的目的是为了提高程序的执行速度,但是并不意味着启动更多的线程会达到更好的并发效果,并发编程还会引起死锁 ,...

  • 《JAVA并发编程的艺术》要点(一)并发编程的挑战

    并发编程的目的是为了让程序运行的更快 并发编程面临的挑战 一、上下文切换问题 二、死锁问题 三、资源受限问题 (一...

网友评论

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

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