前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何预防死锁

如何预防死锁

作者头像
小土豆Yuki
发布2020-11-19 17:39:06
4640
发布2020-11-19 17:39:06
举报
文章被收录于专栏:洁癖是一只狗

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

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

代码语言:javascript
复制

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同时释放转出账户和转入账户这两个资源,

代码语言:javascript
复制

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代码就是按照转入账户和转出账户排序,然后按照序号大小顺序锁定账户,这样就不会产生循环等待

代码语言:javascript
复制

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-11-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 洁癖是一只狗 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档