-
第一部分:概述
我们用经典的“哲学家进餐”问题来理解死锁的概念。五个哲学家坐在一个圆桌旁,他们一共只有五根筷子(不是五双),每两人中间有一根筷子,他们时而思考,时而吃饭,吃完以后把筷子放回原处,好的协调机制可以让他们每个人都可以吃到东西,不好的协调机制,可能会导致他们都饿死。试想一种情况,每个哲学家都拿到一根筷子,他们都期盼着得到别人的筷子,但他们又都不愿放弃自己手中的筷子,这时候就出现了大家相互等待对方释放资源而自己却不释放已有资源的现象。这就是一种典型的死锁。死锁是一类很严重的错误,发生死锁时程序自身无法恢复,只能重启应用,但是重启之后死锁可能还会发生。因此,我们必须在编写程序时就避免死锁这种严重的错误。
-
第二部分:死锁的产生
产生死锁的原因有很多种,通常遇到的有以下几类:
-
锁顺序死锁
我们直接来看例子。两个线程A、B,两个对象a、b,线程A持有对象a的锁,它尝试访问对象b,而线程B持有对象b的锁,他尝试访问对象a,此时就会出现线程A、线程B互相等待对方的现象,这就是锁顺序死锁。
对应的示例代码如下,此代码在运行过程中就存在着极大地线程死锁风险。
public class DeadLock{
private final Object left = new Object();
private final Object right = new Object();
public void leftRight(){
synchronized(left){
synchronized(right){
doSomething();
}
}
}
public void rightLeft(){
synchronized(right){
synchronized(left){
doSomething();
}
}
}
}
-
动态的锁顺序死锁
这类的死锁实质与上面介绍的顺序死锁一样,只不过它们比较隐蔽,只有在动态调用方法是才会遇到。看下面银行转账方法的简单示例:
public void transferMoney(Account from, Account to, BigDecimal amount){
synchronized(from){
synchronized(to){
doSomething();
}
}
}
我们乍看方法觉得不会产生死锁,但是仔细观察你会发现,from和to两个变量的值来自于方法的参数传递,所以会存在这样一种情况:A向B转账时,B也恰好转账给A,此时极有可能产生死锁。所以对此类死锁问题,我们要仔细分析,至于他的解决方法,大家可以思考一下。
-
在协作对象之间发生死锁
在协作对象之间发生的死锁更加隐蔽。例如多个线程安全的synchronized()方法,在使用时出现相互调用的情况,一旦调用的顺序出现循环,那极有可能导致死锁。避免这种死锁的最简单方法就是尽可能的用synchronized代码块取代synchronized方法,使方法尽可能的变成开放调用。
-
资源死锁
多个线程互相持有彼此正在等待的锁而不释放自己持有的锁时就会出现死锁。当他们在相同的资源集合上等待是也会出现死锁。比如以下情况:线程A持有数据库连接池D1的连接,并等待与数据库D2的连接,线程B持有数据库D2的连接,并等待与数据库D1的连接(连接池越大发生这种情况的概率越低)。
-
第三部分:死锁的避免与诊断
分析了上面的例子之后,避免线程死锁其实就变得简单了。我们无非要做的就是避开那些产生死锁的条件即可。
第一,当需要获得多个锁时用一致性的顺序来获取锁。所有需要获得多个锁的操作,都按照一致的顺序获得锁。这样就避免了相互等待对方释放锁的情况。
第二,开放调用。在调用某个方法时不需要持有锁,这种调用叫作开放调用。这也很好理解,比如把synchronized方法移到方法内部变成同步块,这样调用方法时就不需要持有锁,进入方法synchronized块才持有锁。这样做代码更加紧凑。但同时也要注意,原子性操作的代码要封装到一起。
除此之外,尽可能的减少潜在的加锁交互机制,同时将获取锁时需要遵循的协议写进文档都是避免死锁的方法。