Java中的死锁

一、定义

关于死锁的经典描述就是哲学家就餐问题:

假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子。

哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。

二、产生

1、锁顺序死锁

产生

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private final Object left=new Object();
private final Object right=new Object();

public void leftRight(){
synchronized(left){
synchronized(right){
//doSmoething
}
}
}

public void RightLeft(){
synchronized(right){
synchronized(left){
//doSmoethingElse
}
}
}

或者下面这种

1
2
3
4
5
6
7
  public void demo(Object a, Object b){	
synchronized(a){
synchronized(b){
//
}
}
}

如果执行顺序不当,就会发生死锁:

1
2
demo(a, b);
demo(b, a);

解决方法-顺序加锁

解决这种问题的方法就是按顺序加锁,在制定锁的顺序的时候,可以使用System.identityHashCode()方法,该方法返回Object.hashCode返回的值,这样每次都先给hashCode值小的对象上锁就可以避免发生死锁的可能性。

有很小的概率,两个对象的hashCode返回值相同,为了避免这种情况,可以使用”加时赛锁”。在获得两个锁之前,先获得‘’加时赛‘锁,从而保证每次只有一个线程以未知的顺序获得这两个锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static final Object tieLock=new Object();
public void demo(Object a, Object b){

int aHash=System.identityHashCode(a);
int bHash=System.identityHashCode(b);
if(aHash<bHash){
synchronized(a){
synchronized(b){
//
}
}
}else if(aHash>bHash){
synchronized(b){
synchronized(a){
//
}
}
}else{
synchronized(tieLock){
synchronized(a){
synchronized(b){
//
}
}
}
}
}

2、协作对象之间死锁

产生

很多获取锁的操作并不像上面那么明显,这两个锁并不一定在用一个方法中被获取

如果在持有锁时调用某个外部方法,在这个外部方法中可能或获取其他锁(可能造成死锁),或者阻塞时间过长,导致其他线程无法及时获得当前被持有的锁。

解决方法-开放调用

在调用某个方法时不需要持有锁,那么这种调用被称为开放调用

是同步代码块仅被用于那些涉及共享状态的操作,而不是加在整个方法上。

在程序中应尽量使用开放调用。与那些在持有锁时调用外部方法的程序相比,更容易对于依赖于开放性调用的程序进行死锁分析

三、避免

除了上面的几种情况,还有一种方法可以检测死锁和从死锁中恢复过来。

可以使用显示锁来替代内置锁,Lock类的tryLock方法也能避免死锁,在另一篇中已经介绍过了。