在 互斥锁,解决原子性问题 这篇文章中讲到,互斥锁可以将代码串行化,避免线程切换导致的并发问题。当一把锁想要锁住多个资源的时候,要使用更大力度的锁,在 互斥锁,解决原子性问题 转账的例子中使用了类锁 Account.class 来锁住转账双方的账号。
这样一来,A 转账给 B,B 转账给 C,这样两个操作都变成了串行化的。加入每秒有 1 万笔交易,而每个转账操作的操作都是串行的,性能完全不可接受。
在转账操作中,我们使用了类锁 Accont.class 作为互斥锁,而 Accont.class 是所有 Account 对象共用的,这就导致转账操作完全串行化,无法并发。
这里的问题就是锁的范围太大,其实我们不需要锁住所有账户,只需要锁定两个转账账户就就可以了。我们可以使用 细粒度锁,使用细粒度锁可以提高并行度,是性能优化的一个重要手段。 代码如下所示:
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;
}
}
}
}
}
我们给转出账户加一把锁,给转入账户也加一把锁。转账时,先尝试锁定转出账户 this ,锁定成功之后再尝试锁定转入账户 target ,只有当两个账户都被锁定时才会执行转账操作。这样一来,我们即保证了转账操作不会出现金额问题,也能让多个账户之间的转账操作并发执行。
死锁指的是, 一组互相竞争资源的线程因互相等待,导致「永久」阻塞的现象 。
以上面的 transfer() 方法为例,线程 T1 执行 A 向 B 转账的操作,线程 T2 执行 B 向 A 转账的操作。如果线程 T1 和 T2 同时执行 transfer() 会导致这样一个线程:
此时就造成了 死锁的局面 :线程 T1 和 T2 互相持有对方需要的资源,同时也都需要对方所持有的资源。
什么情况下会产生死锁呢?有个叫 Coffman 的牛人早就总结过了,只有以下这 4 个条件都发生时才会出现死锁:
只有在互斥、占有且等待、不可抢占、循环等待这 4 个条件都满足的条件下才会产生死锁,所以我们 只要破坏其中一个条件就可以避免死锁 。
| 描述 | 解决方案 | |
|---|---|---|
| 互斥 | 共享资源 X 和 Y 只能被一个线程占用; | / |
| 占有且等待 | 线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X; | 一次申请所有资源 |
| 不可抢占 | 其他线程不能强行抢占线程 T1 占有的资源; | 主动释放线程占有的资源 |
| 循环等待 | T1 等待 T2 占有的资源,T2 等待T1 占有的资源 | 按序申请资源:先申请资源需要小的,再申请资源需要大的 |
一次申请所有资源,就可以破坏占有且等待条件。也就是不允许账户 A 自己去加锁和解锁,也不允许账户 B 自己去加锁和解锁,而是由一个管理员管理所有账户的锁。管理员则保证一次申请所有资源,即同时拿到账户 A 和账户 B 的锁。
对应到编程领域,“同时申请”这个操作是一个 临界区 ,我们也需要一个角色(Java 里面的类)来管理这个临界区,我们就把这个角色定为 Allocator。它有两个重要功能,分别是:
具体的代码实现如下:
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)
}
}
}
破坏不可抢占条件的方式是,主动释放线程占有的资源,即线程 T1 持有资源的情况下请求其他资源,如果无法获得请求的资源,则主动释放自己的占有的资源。
synchronized 主动做到主动释放资源,因为 synchronized 申请资源的时候,如果申请不到就会直接进入阻塞状态。进入阻塞状态的线程什么也不会做,也就不会主动释放资源。
但是我们可以使用 java.util.concurrent 包下的 Lock 来解决这个问题。
破坏循环等待条件,可以采用对资源进行排序的方式。以转账操作为例,我们可以给每个账户分配一个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;
}
}
}
}
}