如何预防死锁

上一篇我们使用Account.class作为互斥锁,解决了银行转账的问题,但是我们发现这样的转账操作就变成了串行,这样对于性能就会大打折扣,现实生活中这种是不能别接受的。

事实上,并发并发编程中,转账的这种情况,需要两把锁,这样就可以实现并发,例如,我们把账户A转入账户B的场景,此时我们可以建立两把锁,分别锁住账户A和账户B,代码如下

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

相对于上一篇,我们说使用Account.class作为互斥锁,锁定的范围太大,而我们锁定两个账户范围小很多,这样的锁,就叫做细粒度锁,使用细粒度锁可以提高并发度,是性能优化的一个重要手段。

看上去这样已经很完美了,但是实际上,是有代价的,这个代价就是可能导致死锁.如下图

张三要从账户A转给账户B100元,而同时李四要从账户B给账户A转入100元,但是张三拿到账户A的时候,发现账户B是被李四拿到了,就会等待,同时李四拿到账户B的时候,发现账户A却被李四拿走了,也就会等待。

上面就是就会产生死锁,死锁的专业定义就是,一组互相竞争资源的线程因互相等待,导致永久阻塞的现象

如何预防死锁

首先解决问题之前,我们先要知道如何发生死锁,下面四个条件同时产生就会产生死锁,

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

只要我们破坏其中一条就可以了,因为锁的本质就是利用互斥,所以没有办法破坏,不过其他三个条件都是有办法破坏的,

  1. 对于占有等待,我们可以一次性申请所有资源,这样就不存在等待了
  2. 对于不可抢占,占有的资源进一步申请其他资源时候,如果申请不到,可以主动放弃他占有的资源,这样不可抢占这个条件就可以破坏
  3. 对于循环等待,可以按照顺序申请来预防,申请的时候可以申请序号小的,在申请序号大的,这样就不会产生循环等待了。

破坏占用且等待条件

就那上面的账户A和账户B来说,我们一次性把申请所有账号,因此我们需要一个角色管理这个操作,此时我们可以在账户Account类里面持有一个Allocator的单例(必须是单例,只能有一个人分配资源),当账户在执行转账操作的时候,首先向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)
    }
  } 
}

破坏不可抢占条件

破坏不可抢占条件看起来很简单,核心就是主动放弃他占有的资源,但是这一点synchronizd是做不到的,原因是由于synchronized申请资源的时候,如果申请不到,就会阻塞等待,也释放不了线程已经占有的资源,但是java,提供另外一种解决办法,就是java.util.concurrent包下面的lock就可已解决这个问题

破坏循环等待条件

破坏这个条件,需要对资源进行排序,然后按序申请资源,我们假设账户有一个属性id,我们根据这个字段id进行排序,申请的时候,我们可以按照从小到大的顺序申请,如下面代码,1-6代码就是按照转入账户和转出账户排序,然后按照序号大小顺序锁定账户,这样就不会产生循环等待

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

预防死锁就是破坏三个条件中的一个有了这个思路后,实现就简单,但是我们仍然注意的是,防止死锁的成本也是很高的,比如破坏占用且等待条件的成本要大于破坏循环等待的成本,因为破坏占用且等待条件实在循环获取多个资源,直到获取,因此破坏循环等待就是一个成本最低的方案

本文分享自微信公众号 - 洁癖是一只狗(rookie-dog),作者:洁癖汪

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-11-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 并发编程如何使用锁保护多个资源

    上一篇我们知道受保护资源和锁之间合理的关联关系应该是N:1的关系,也就是说一个锁可以保护多个资源,并不能多把锁来保护一个资源,今天我们就说说如何实现一把锁保护多...

    小土豆Yuki
  • Mysql幻读如何解决

    sessionA中添加了三条相同的语句,都是给d=5这行添加行锁,且使用当前读,而上面运行的结果如下

    小土豆Yuki
  • 手把手教你搭建 MySQL 主从复制经典架构(一主一从、主主、一主多从、多主一从)

    1、做数据的热备,作为后备数据库,主数据库服务器故障后,可切换到从数据库继续工作,避免数据丢失。 2、架构的扩展。业务量越来越大,I/O访问频率过高,单机无法满...

    小土豆Yuki
  • 深入解析Java垃圾回收机制引入垃圾回收哪些内存需要回收?如何回收为什么需要分代收集?JVM的分代分代垃圾收集过程详述

    自动垃圾回收机制就是寻找Java堆中的对象,并对对象进行分类判别,寻找出正在使用的对象和已经不会使用的对象,然后把那些不会使用的对象从堆上清除。 自动垃圾回收...

    desperate633
  • Say No to Loop!

    本文会介绍下Eloquent\Collection,这个是什么呢?这是我们平常使用Eloquent中get,all返回的结果集。

    zhuanxu
  • 看人识人 - 设计师辅技手册(一)

    腾讯ISUX
  • 初学C语言?先搞懂这些基础知识再谈深度学习吧!

    编译程序: 如何把源程序转换成机器能够接受的目标程序,软件工作者编制了一系列的软件.通过这些软件,把用户按规定语法写出的语句一一翻译成二进制的机器指令. 这种具...

    老九君
  • 技术流派:物联网IoT的技术落地

    魏新宇
  • 5金1银且团队第一,中国队蝉联2020国际数学奥赛,但下届可能就不只有人类参赛了

    2020 年国际数学奥林匹克竞赛(IMO)成绩出炉!中国队获得 5 金一银,成绩获得团体总分第一名。这是中国队在 2019 年和美国队获得并列第一后,再度蝉联冠...

    新智元
  • 三星和魅蓝同一天发新机,却讲了两个截然不同的故事

    8月23日晚,三星年度双旗舰GalaxyNote 8发布,成为安卓圈的大事件,市场给予Note 8高度关注,以至于当天白天发布的魅蓝Note 6,竟显得有些落寞...

    罗超频道

扫码关注云+社区

领取腾讯云代金券