多线程-6.死锁的四个产生条件以及破坏死锁的方法
产生死锁的四个必要条件:
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 }