并发编程之死锁
接上一篇文章转账操作死锁问题:https://blog.csdn.net/weixin_39707461/article/details/133893488?spm=1001.2014.3001.5502
class Account {
private int balance;
// 转账
void transfer(Account target, int amt){
// 锁定转出账户
synchronized(this) { //1)
// 锁定转入账户
synchronized(target) { //2)
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
}
回忆:
线程1(A给B转账):1)锁住 账户A,2)锁住 账户B;
线程2(B给C转账):1)锁住 账户B,2)锁住 账户A;
线程1 与 线程2 互斥,因为不可能都同时获取到账户A和账户B的锁,所以不可以发生在同一时刻。
总结:该方法可以保证并发安全。但是,如果当 线程1 获取到 账户A 的锁(等待线程2释放 账户B 的锁),线程2 获取到 账户B 的锁(等待线程2释放 账户B 的锁)。两个线程就陷入了死锁状态。
死锁产生的几个条件
1:占有且等待,线程1 已经取得共享资源 账户A 对象,在等待共享资源 账户B 对象的时候,不释放共享资源 账户A;
2:不可抢占,其他线程不能强行抢占 线程1 占有的资源;
3:循环等待,线程1 等待 线程2 占有的资源,线程2 等待 线程1 占有的资源,就是循环等待。
我们只要破坏其中一个条件,就可以成功避免死锁的发生。
破坏 占有且等待 条件
对于“占用且等待”这个条件,如果可以一次性申请所有的资源,这样就不存在等待了。
实现思路:定义一个全局唯一的资源分配者,它有两个重要功能,分别是:同时申请资源 apply() 和同时释放资源 free()
// 全局唯一的分配者
class Allocator {
private List<Object> als = new ArrayList<>();
// 一次性申请所有资源,该方法只能是串行的
synchronized boolean apply(Object accountFrom, Object accountTo) {
//如果资源已经被 其它线程申请,则申请失败
if(als.contains(accountFrom) || als.contains(accountTo)){
return false;
} else {
//两个资源都是空闲的,允许申请
als.add(accountFrom);
als.add(accountTo);
}
return true;
}
// 归还资源
synchronized void free(Object accountFrom, Object accountTo){
als.remove(from);
als.remove(to);
}
}
class Account {
// actr应该为单例
private Allocator actr;
private int balance;
// 转账
void transfer(Account target, int amt){
// 一次性申请转出账户和转入账户,直到成功,注意这里死循环获取的缺点是一直获取不到,会一直占用CPU资源
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)
}
}
}
破坏不可抢占条件
使用 等待-通知 机制优化, 使用 synchronized 配合 wait()、notify()、notifyAll() 这三个方法可以快速实现这种机制:
class Allocator {
private List<Object> als;
// 一次性申请所有资源
synchronized void apply(
Object from, Object to){
// 经典写法
while(als.contains(from) || als.contains(to)){
try{
//主动让出锁
wait();
}catch(Exception e){
}
}
als.add(from);
als.add(to);
}
// 归还资源
synchronized void free(Object from, Object to){
als.remove(from);
als.remove(to);
notifyAll();
}
}
破坏循环等待条件
破坏这个条件,需要对资源进行排序,然后按序申请资源。
这个实现非常简单,我们假设每个账户都有不同的属性 id,这个 id 可以作为排序字段,申请的时候,我们可以按照从小到大的顺序来申请。这样就不存在“循环”等待了。
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;
}
}
}
}
}