前面说使用Account.class作为转账的互斥锁,这种情况下所有的操作都串行化,性能太差,这个时候需要提升性能,肯定不能使用这种方案.
假设某个账户的所有操作都在账本中,那转账操作需要两个账户,这个时候有三种情况:
上面的逻辑其实就是使用两把锁实现,图形化:
代码实现如下:
class Account {
private int balance;
// 转账
void transfer(Account target, int amt){
// 锁定转出账户
synchronized(this) {
// 锁定转入账户
synchronized(target) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
}
这个其实就是细粒度锁. 细粒度锁提高并行度,是性能优化的一个重要手段.,但是天下没有免费的午餐,这种细粒度锁可能会导致死锁的情况发生.也就是假如现在A转账100给B,由张三做这个转账业务;B转账给A100元,由李四完成这个转账业务,这个时候张三拿到A的账户本,同一时刻李四拿到B的账户本,这个时候张三等待李四的B账户本,李四等待张三的A账户本,两人都不会送回来,就产生的死等,死等就是变成领域的死锁.死锁是指一组互相竞争资源的线程因互相等待,导致永久阻塞的现象
死锁一旦产生是没有办法解决的,只能重启应用. 所以解决死锁的最好办法就是避免死锁,如何避免死锁,那就要从产生死锁的条件入手:
四个添加同时满足就会产生死锁,只要能破坏掉有一个条件,死锁就不会产生.共享资源是没有办法破坏,也就是互斥是没有办法解决,锁的目的就是为了互斥.
只需要同时申请资源就可以,同时申请这个操作是一个临界区,需要一个Java类来管理这个临界区,也就是定义一个角色,这个角色的两个重要功能就是同时申请资源apply()和同时释放资源free(),并且这个类是单例的.其实本质就是设置一个管理员,只有管理员有权限去分配资源,其他普通用户只能去管理员那取资源,一个人操作就不会产生死锁了.
代码实现如下:
class Allocator {
private List<Object> als =
new ArrayList<>();
// 一次性申请所有资源
synchronized boolean apply(
Object from, Object to){
if(als.contains(from) ||
als.contains(to)){
return false;
} else {
als.add(from);
als.add(to);
}
return true;
}
// 归还资源
synchronized void free(
Object from, Object to){
als.remove(from);
als.remove(to);
}
}
class Account {
// actr 应该为单例
private Allocator actr;
private int balance;
// 转账
void transfer(Account target, int amt){
// 一次性申请转出账户和转入账户,直到成功
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是做不到,因为synchronized申请资源的时候如果申请不到就直接进入阻塞,阻塞状态啥也干不了.
这个时候就需要java.util.concurrent包下提供的Lock,这个等学到的时候再总结.
这个就需要一个id值了,保护加锁的顺序是从序号小的资源开始.
class Account {
private int id;
private int balance;
// 转账
void transfer(Account target, int amt){
Account left = this; ①
Account right = target; ②
// left是序号小的资源锁
if (this.id > target.id) { ③
left = target; ④
right = this; ⑤
} ⑥
// 锁定序号小的账户
synchronized(left){
// 锁定序号大的账户
synchronized(right){
if (this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
}
}
保证课加锁的顺序,就不会出现循环等待了.
编程世界其实是和现实世界有所关联的,编程不就是为了解决现实生活中的问题吗? 上面的解决死锁的两个方案,那个更好呢? 其实破坏循环等待条件的成本要比破坏占有且等待的成本要低,后者也锁定了所有账户并且使用了死循环.相对来说,前者的成本低,但是不是绝对的,只是转账的这个例子中,破坏循环等待的成本比较低.
原文:https://www.cnblogs.com/wadmwz/p/10504201.html