在 互斥锁,解决原子性问题 这篇文章中讲到,互斥锁可以将代码串行化,避免线程切换导致的并发问题。当一把锁想要锁住多个资源的时候,要使用更大力度的锁,在 互斥锁,解决原子性问题 转账的例子中使用了类锁 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; } } } } }