05-如何避免死锁
死锁现象:
最常见的就是转账操作:B转A的同时,A转账给B,那么先锁B再锁A,但是,另一个线程是先锁A再锁B,然而,如果两个线程同时执行,那么就是出现死锁的情况,线程T1锁了A请求锁B,此时线程T2锁了B请求锁A,都在等着对方释放锁,然而自己都不会释放锁,故死锁。
死锁发生的条件:
- 占用且等待;
- 已锁定的资源不可抢占;
- 循环等待
避免的方法:
一、破坏循环等待的条件按序申请资源
最简单的办法,假设每个账户都有不同的属性id,这个id可以作为排序字段。就是无论哪个线程执行的时候,都按照顺序加锁,即按照A和B的id大小来加锁,这样,无论哪个线程执行的时候,都会先加锁A,再加锁B,A被加锁,则等待释放。另外的线程只能先等A被释放,再申请A的锁,这样就不会被死锁了。
1 public class Account { 2 private int id; //每个账户都有不同的属性id,这个 id 可以作为排序字段 3 private int balance; //账户余额 4 /** 5 * 转账 6 * @param target 目标账户 7 * @param amt 转账金额 8 */ 9 void transfer(Account target, int amt){ 10 Account left = this; //当前账户(转出账户) 11 Account right = target; 12 if (this.id > target.id) { 13 left = target; 14 right = this; 15 } 16 // 锁定序号小的账户 17 synchronized(left){ 18 // 锁定序号大的账户 19 synchronized(right){ 20 if (this.balance > amt){ 21 this.balance -= amt; 22 target.balance += amt; 23 } 24 } 25 } 26 } 27 }
二、破坏不可抢占条件
要主动释放锁定的资源,synchronized无法做到。而java.util.concurrent包下的Lock是可以轻松解决这个问题的。
三、破坏占用且等待条件
要破坏占用且等待条件,可以一次申请所有资源。比如转账操作,可以增加一个账本管理员,柜员要拿到账本只能通过管理员。A转给B,需要柜员(线程)拿到账本A和账本B,如果此时账本管理员手里只有账本B,这个时候是不会给他账本A的,只有同时拥有A和B时,才会给他。
1 import java.util.ArrayList; 2 import java.util.List; 3 4 class Allocator { 5 private List<Object> als = new ArrayList<>(); 6 // 一次性申请所有资源 7 synchronized boolean apply(Object from, Object to){ 8 if(als.contains(from) || als.contains(to)){ 9 return false; 10 } else { 11 als.add(from); 12 als.add(to); 13 } 14 return true; 15 } 16 // 归还资源 17 synchronized void free(Object from, Object to){ 18 als.remove(from); 19 als.remove(to); 20 } 21 22 class Account1 { 23 // actr 应该为单例 24 private Allocator actr; 25 private int balance; //账户余额 26 // 转账 27 void transfer(Account1 target, int amt){ 28 // 一次性申请转出账户和转入账户,直到成功 29 while(!actr.apply(this, target)); 30 try{ 31 // 锁定转出账户 32 synchronized(this){ 33 // 锁定转入账户 34 synchronized(target){ 35 if (this.balance > amt){ 36 this.balance -= amt; 37 target.balance += amt; 38 } 39 } 40 } 41 } finally { 42 actr.free(this, target); 43 } 44 } 45 } 46 }

浙公网安备 33010602011771号