首页 > 技术文章 > 多线程-6.死锁的四个产生条件以及破坏死锁的方法

ningbing 2021-03-12 15:20 原文

产生死锁的四个必要条件:
  1.互斥条件:一个资源同一时刻只能被一个线程所占有。
  2.持有并等待条件:一个线程T1已经持有某资源X,然后申请获得新的资源Y,在等待过程中不释放已有资源X。
  3.不可抢占条件:其它线程不能强行抢占线程T1的资源。
  4.循环等待条件:线程T1持有资源X,等待线程T2持有的资源Y,线程T2等待X。
死锁的例子
  假设线程 T1 执行账户 A 转账户 B 的操作,账户 A.transfer(账户 B);同时线程 T2 执行账户 B 转账户 A 的操作,账户 B.transfer(账户 A)。当 T1 和 T2 同时执行完①处的代码时,T1 获得了账户 A 的锁(对于 T1,this 是账户 A),而 T2 获得了账户 B 的锁(对于 T2,this 是账户 B)。之后 T1 和 T2 在执行②处的代码时,T1 试图获取账户 B 的锁时,发现账户 B 已经被锁定(被 T2 锁定),所以 T1 开始等待;T2 则试图获取账户 A 的锁时,发现账户 A 已经被锁定(被 T1 锁定),所以 T2 也开始等待。于是 T1 和 T2 会无期限地等待下去,也就是我们所说的死锁了。
 1 class Account {
 2   private int balance;
 3   // 转账
 4   void transfer(Account target, int amt){
 5     // 锁定转出账户
 6     synchronized(this){     ①
 7       // 锁定转入账户
 8       synchronized(target){ ②
 9         if (this.balance > amt) {
10           this.balance -= amt;
11           target.balance += amt;
12         }
13       }
14     }
15   } 
16 }
破坏死锁的方法:
  互斥条件:不可改变。
  持有并等待条件:可以一次性给线程申请所有的资源。
  不可抢占条件:线程在申请不到资源时主动释放掉已有的资源。
  循环等待条件:将资源线性排列,申请时先申请序号小的,再申请序号大的。
破坏不可抢占条件:
  synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。Java 在语言层次无法解决这个问题,不过在 SDK 层面还是解决了的。java.util.concurrent 这个包下面提供的 Lock 是可以轻松解决这个问题的。
破坏循环等待条件:
  假设每个账户都有不同的属性 id,这个 id 可以作为排序字段,申请的时候,可以按照从小到大的顺序来申请。比如下面代码中,①~⑥处的代码对转出账户(this)和转入账户(target)排序,然后按照序号从小到大的顺序锁定账户。这样就不存在“循环”等待了。
  
 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 }

推荐阅读