1)上一节我们用一把大锁锁住银行的转账业务,这样会造成什么样的问题?
- 所有账户的转账操作都是串行的,性能太差
- A 转账户 B、账户 C 转账户 D 这两个转账操作现实世界里是可以并行的,但是在这个方案里却被串行化了
2)那么如何优化可以让我们的账户之间转账和入账能够并行执行呢?
- 在 transfer() 方法内部,我们首先尝试锁定转出账户 this(先把转出账本拿到手),然后尝试锁定转入账户 target(再把转入账本拿到手),只有当两者都成功时,才执行转账操作。
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 转账户 B 和账户 C 转账户 D 这两个转账操作就可以并行了
3)上面我们使用了细粒度锁,那可能会导致什么代价?
- 死锁
4)死锁的四个必要条件?
- 互斥资源
- 循环等待
- 请求与保持
- 不可剥夺
- 对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。
- 对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
- 对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。
- 一次性申请所有资源
- 增加一个账本管理员,然后只允许账本管理员从文件架上拿账本,当张三要AB的时候,管理员看看架子,只有AB都在才会给张三,否则不会给
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 在语言层次确实没有解决这个问题,不过在 SDK 层面还是解决了的,java.util.concurrent 这个包下面提供的 Lock 是可以轻松解决这个问题的
- 对资源进行排序,然后按序申请资源
- 锁排序法
- 不局限于当下,可以换个思路,向现实世界要答案,利用现实世界的模型来构思解决方案