在互斥锁,解决原子性问题这篇文章中讲到,互斥锁可以将代码串行化,避免线程切换导致的并发问题。当一把锁想要锁住多个资源的时候,要使用更大力度的锁,在互斥锁,解决原子性问题转账的例子中使用了类锁 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 持有账户 A,需要请求账户 B
- 线程 T2 持有账户 B,需要请求账户 A
此时就造成了死锁的局面:线程 T1 和 T2 互相持有对方需要的资源,同时也都需要对方所持有的资源。
什么情况下会产生死锁呢?有个叫 Coffman 的牛人早就总结过了,只有以下这 4 个条件都发生时才会出现死锁:
- 互斥,共享资源 X 和 Y 只能被一个线程占用;
- 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
- 不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
- 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。
如何避免死锁
只有在互斥、占有且等待、不可抢占、循环等待这 4 个条件都满足的条件下才会产生死锁,所以我们只要破坏其中一个条件就可以避免死锁。
描述 | 解决方案 | |
---|---|---|
互斥 | 共享资源 X 和 Y 只能被一个线程占用; | \ |
占有且等待 | 线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X; | 一次申请所有资源 |
不可抢占 | 其他线程不能强行抢占线程 T1 占有的资源; | 主动释放线程占有的资源 |
循环等待 | T1 等待 T2 占有的资源,T2 等待T1 占有的资源 | 按序申请资源:先申请资源需要小的,再申请资源需要大的 |
破坏占有且等待条件
一次申请所有资源,就可以破坏占有且等待条件。也就是不允许账户 A 自己去加锁和解锁,也不允许账户 B 自己去加锁和解锁,而是由一个管理员管理所有账户的锁。管理员则保证一次申请所有资源,即同时拿到账户 A 和账户 B 的锁。
对应到编程领域,“同时申请”这个操作是一个临界区,我们也需要一个角色(Java 里面的类)来管理这个临界区,我们就把这个角色定为 Allocator。它有两个重要功能,分别是:
- 同时申请资源 apply() 和同时释放资源 free()。账户 Account 类里面持有一个 Allocator 的单例(必须是单例,只能由一个人来分配资源)。
- 当账户 Account 在执行转账操作的时候,首先向 Allocator 同时申请转出账户和转入账户这两个资源,成功后再锁定这两个资源;当转账操作执行完,释放锁之后,我们需通知 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;
}
}
}
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。