并发编程—死锁了,怎么办?
上一篇文章中提到了如果多个资源之间不存在关系时,尽量使用细粒度的锁,但是在实际应用中,使用细粒度的锁有时会付出惨重代价的,这个代价就是可能造成可怕的“死锁”。
那么什么是死锁呢?
死锁是指一组互相竞争资源的线程因为互相等待,导致“永久”阻塞的现象。
如何预防死锁
并发程序一旦死锁,一般没有特别好的方法,很多时候我们只能重启应用。因此,解决死锁问题做好的办法还是规避死锁。
那如何避免死锁呢?要避免死锁就需要先分析死锁发生的条件,只有以下四个条件都发生时才会出现死锁:
- 互斥,共享资源X和Y只能被一个线程占用。
- 占有且等待,线程T1已经取得贡献资源X,在等待共享资源Y的时候,不释放资源X。
- 不可抢占,其他线程不能强行抢占线程T1占有的资源
- 循环等待,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待。
反过来分析,也就是说我们只要破坏其中一个,就可以成功避免死锁的发生。
其中,互斥这个条件我们是无法破坏,因为我们使用锁的目的就是互斥。其他三个条件都有办法破坏掉,那如何做呢?
- 对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。
- 对应“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这个不可抢占这个条件就破坏掉了。
- 对于“循环等待”这个条件,可以靠申请资源来预防,也就是资源是有线性顺序的,申请的时候可以先申请序号小的,再申请序号大的,这样线性化后就不会存在循环了。
下面我们就具体分析一下在实际编码中如何操作
1、破坏占用且等待条件
从理论上讲,要破坏这个条件,可以一次性申请所有资源。那么如何才能一次性的获取所有的资源呢,我们可以添加一个锁管理员,也就是每次申请资源时,都向锁管理员申请,释放锁也都一起把资源归还给管理员。我们定义一个类定义为 Allocator,他有两个功能,一个是apply()申请资源和free() 释放资源。如下所示:
public class Allocator {
private List<Object> als = new ArrayList<>();
public 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;
}
}
public synchronized void free(Object from, Object to){
als.remove(from);
als.remove(to);
}
}
public class Account {
private Allocator allocator; //必须保证单例性
private int balance;
// 转账
void transfer(Account tar, int amt) {
while (!allocator.apply(this, tar)) ;
synchronized (this) {
try {
this.balance -= amt;
tar.balance += amt;
} finally {
allocator.free(this, tar);
}
}//while
}//transfer
}
2、破坏不可抢占条件
破坏不可抢占条件看上去很简单,核心是能够主动释放他占有的资源,这一点synchronized是做不到的。原因是synchronized申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已占有的资源。
所以在JDK1.5后的版本中的java.util.concurrent包下提供了 Lock 显示锁,可以轻松的解决这个问题。
3、破坏循环等待条件
破坏这个条件,需要对资源进行排序,然后按需申请资源。具体操作和为每个资源设置一个编号,在申请锁的时候,我们按从小到大的顺序来申请。如下所示:
public class Account {
private int id;
private int balance;
// 转账
void transfer(Account tar, int amt) {
Account left = this;
Account right = tar;
if(this.id > tar.id){
right = this;
left = tar;
}
synchronized (left) {
synchronized (right){
this.balance -= amt;
tar.balance += amt;
}
}
}
}
总结
我们今天这一篇文章主要讲了用细粒度锁来锁定多个资源时,要注意死锁的问题。这个就需要你能把它强化为一个思维定势,遇到这种场景,马上想到可能存在死锁问题。当你知道风险之后,才有机会谈如何预防和避免,因此,识别出风险很重要。
再者就是,在选择具体方案的时候,还要评估一下操作成本,从中选择一个成本最低的方案