java并发编程实战《五》死锁 <挑战打卡60天>

2021-01-06 03:30

阅读:460

  •   对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。

那具体如何体现在代码上呢?

  破坏 占用且等待 条件

    从理论上讲,要破坏这个条件,可以一次性申请所有资源。在现实世界里,就拿前面我们提到的转账操作来讲,它需要的资源有两个,一个是转出账户,另一个是转入账户,当这两个账户同时被申请时,我们该怎么解决这个问题呢?

    可以增加一个账本管理员,然后只允许账本管理员从文件架上拿账本,也就是说柜员不能直接在文件架上拿账本,必须通过账本管理员才能拿到想要的账本。这样就保证了“一次性申请所有资源”。(解决不了的问题就再加一个中间层?)

    

      “同时申请”这个操作是一个临界区,我们也需要一个角色(Java 里面的类)来管理这个临界区,我们就把这个角色定为 Allocator。它有两个重要功能,分别是:同时申请资源 apply() 和同时释放资源 free()。

      账户 Account 类里面持有一个 Allocator 的单例(必须是单例,只能由一个人来分配资源)。

      当账户 Account 在执行转账操作的时候,首先向 Allocator 同时申请转出账户和转入账户这两个资源,成功后再锁定这两个资源;当转账操作执行完,释放锁之后,我们需通知 Allocator 同时释放转出账户和转入账户这两个资源。

    具体的代码实现如下:

      

 1 class Allocator {
 2   private List als =  new ArrayList();
 3   // 一次性申请所有资源
 4   synchronized boolean apply(
 5     Object from, Object to){
 6     if(als.contains(from) ||
 7          als.contains(to)){
 8       return false;  
 9     } else {
10       als.add(from);
11       als.add(to);  
12     }
13     return true;
14   }
15   // 归还资源
16   synchronized void free(
17     Object from, Object to){
18     als.remove(from);
19     als.remove(to);
20   }
21 }
22 
23 class Account {
24   // actr应该为单例 //这个单例怎么实现?
25   private Allocator actr;
26   private int balance;
27   // 转账
28   void transfer(Account target, int amt){
29     // 一次性申请转出账户和转入账户,直到成功
30     while(!actr.apply(this, target)) // 原理类似CAS,也是自旋,实际项目中需要加入超时时间,避免一直阻塞
31 32     try{
33       // 锁定转出账户
34       synchronized(this){              
35         // 锁定转入账户
36         synchronized(target){           
37           if (this.balance > amt){
38             this.balance -= amt;
39             target.balance += amt;
40           }
41         }
42       }
43     } finally {
44       actr.free(this, target)
45     }
46   } 
47 }

 

  破坏 不可抢占 条件

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

    Java 在语言层次确实没有解决这个问题,不过在 SDK 层面还是解决了的,java.util.concurrent 这个包下面提供的 Lock 是可以轻松解决这个问题的。

    简单说一下synchronized的原理?

  

  破坏 循环等待 条件

  破坏这个条件,需要对资源进行排序,然后按序申请资源

    我们假设每个账户都有不同的属性 id,这个 id 可以作为排序字段,申请的时候,我们可以按照从小到大的顺序来申请(大到小也行,重点是排序)。

    

 1 class Account {
 2   private int id;
 3   private int balance;
 4   // 转账
 5   void transfer(Account target, int amt){
 6     Account left = this; 7     Account right = target;    ②
 8     if (this.id > target.id) { ③
 9       left = target;           ④
10       right = this;            ⑤
11     }                          ⑥
12     // 锁定序号小的账户
13     synchronized(left){
14       // 锁定序号大的账户
15       synchronized(right){ 
16         if (this.balance > amt){
17           this.balance -= amt;
18           target.balance += amt;
19         }
20       }
21     }
22   } 
23 }

 

总结

  当我们在编程世界里遇到问题时,应不局限于当下,可以换个思路,向现实世界要答案,利用现实世界的模型来构思解决方案,这样往往能够让我们的方案更容易理解,也更能够看清楚问题的本质。

  识别出风险很重要。

识别出风险很重要。

  我们在选择具体方案的时候,还需要评估一下操作成本,从中选择一个成本最低的方案。

 

课后思考

  我们上面提到:破坏占用且等待条件,我们也是锁了所有的账户,而且还是用了死循环 while(!actr.apply(this, target));这个方法,那它比 synchronized(Account.class) 有没有性能优势呢?

  引自极客用户:

    虽然上面两种锁的方式都是串行化了,但是具体还是有一点区别的:synchronized(Account.class)的方式相当于A->B 转账,C->D转账 先后执行,而 actr.apply(this, target)的方式则是apply-->转账-->free这样的串行方式执行,但是在转账中是可以A->B,C->D转账线程并行执行的,正如文中提到的apply方法耗时很少 所以比如一次转账耗时200ms,apply+release方式执行要20ms,所以用synchronized的方式A->B,C->D则需要耗时400ms,而appy的方式则要200+20*2=240ms,并且同时转账的人越多 apply方式的转账并行度越高 比synchronized的方式的优势越明显。


评论


亲,登录后才可以留言!