线程死锁
安全性和活跃度通常是相互制约的,虽然可以通过开辟一定量的线程来提高活跃度,但是用来保证多线程安全的锁也可能引起锁顺序死锁问题(lock-ordering deadlock)。类似,我们使用线程池和信号量来约束资源的使用,但是也可能存在资源死锁(resource deadlock)。Java程序不能从死锁中恢复,所以能够避免死锁对于程序的设计十分重要。
死锁
哲学家就餐是多线程中很经典的问题,它就存在死锁风险。五位哲学家都抓住右边的叉子,同时等待左边的叉子而不放弃右边的叉子时,那么任意一位哲学家都吃不到面,从而饿死。在Executor任务执行中提交了子任务,同时当前线程池没有多余的线程可供使用,而母线程等待子线程的结果,子线程等待母线程的计算资源时,也会出现死锁问题。此外,当线程A占有对象锁L时,想要获得另一个对象锁M,线程B持有对象锁M,想要获得对象锁L,两个线程将永远等待下去,这种彼此等待的死锁称为“依赖死锁”。
当一个线程永远占有另一个线程所等待的锁,而另一个线程尝试去获得这个锁,那么它们将永远被阻塞。
- 锁顺序死锁
哲学家就餐问题中,每位哲学家在尝试获取叉子时,如果顺序不一致(不是按照一个方向,而是每位哲学家任意挑选左右两边的一只筷子),那么就可能导致锁顺序死锁问题,如下图所示。
验证锁顺序的一致性需要对程序中锁的行为进行分析,单独观察每一个锁的代码路径并不能得出锁顺序死锁的结论。
public class Test {
public static void main(String[] args) {
Left left = new Left();
Right right = new Right();
left.setRight(right);
right.setLeft(left);
left.start();
right.start();
}
public static class Left extends Thread{
private Right right;
public void setRight(Right right) {
this.right = right;
}
public void synchronizedLeft() {
synchronized(this) {
try {
Thread.sleep(10);
//如果把try...catch放到synchronized里面呢?
synchronized(right) {
right.sayMyName();
}
} catch(Exception e) {
//操作
}
}
}
public void sayMyName() {
System.out.println("my name is Left");
}
@Override
public void run() {
this.synchronizedLeft();
}
}
public static class Right extends Thread{
private Left left;
public void setLeft(Left left) {
this.left = left;
}
public void synchronizedRight() {
synchronized(this) {
try {
Thread.sleep(10);
//如果把try...catch放到synchronized里面呢?
synchronized(left) {
left.sayMyName();
}
} catch(Exception e) {
//操作
}
}
}
public void sayMyName() {
System.out.println("my name is Riht");
}
@Override
public void run() {
this.synchronizedRight();
}
}
}
从上面的图片和代码中可以看出,锁顺序死锁造成原因是,线程A先锁定Left,在尝试获取Right锁时,Right锁已经被B线程锁定了,而B线程又在等待A线程释放Left锁。所以,如果线程A先锁定了Left,又在B之前锁定了Right,那么锁顺序死锁就不会发生。问题关键,是如何保证获取锁时,另一个线程不会占用资源。
银行进行转账时,两个账户间会不会有死锁问题存在?这也是死锁比较经典的场景。这种常见也比较明显,就是在同一个方法中获取两个锁。
- 协作对象间的死锁
锁顺序死锁在代码分析上比较容易察觉,因为锁的获取口径比较直观,即synchronized嵌套了另一个synchronized。协作对象间的死锁,则不易察觉,因为对象间的方法调用带来了synchronized的嵌套。锁顺序是一种显示的锁定,而协作对象间的锁是隐士的锁顺序。
public class Test {
public static void main(String[] args) {
final Left left = new Left();
final Right right = new Right();
left.setRight(right);
right.setLeft(left);
left.start();
right.start();
}
public static class Left extends Thread{
private Right right;
public void setRight(final Right right) {
this.right = right;
}
/**
* 从方法调用上,很难一眼看出synchronized嵌套
*/
public synchronized void sayMyName1() {
try {
Thread.sleep(10);
/*
* 这里是对象间协作时发生了死锁,因为Left获取了锁后,在right中有尝试获取锁,
* 也就是隐士synchronized嵌套,这种称为对象协作间死锁
*/
this.right.sayMyName1();
} catch(Exception e) {
//操作
}
}
public synchronized void sayMyName2() {
System.out.println("you name is Right");
}
public void run() {
this.sayMyName1();
}
}
public static class Right extends Thread{
private Left left;
public void setLeft(final Left left) {
this.left = left;
}
public synchronized void sayMyName1() {
System.out.println("you name is Left");
}
/**
* 从方法调用上,很难一眼看出synchronized嵌套
*/
public synchronized void sayMyName2() {
try {
Thread.sleep(10);
/*
* 这里是对象间协作时发生了死锁,因为Right获取了锁后,在left中有尝试获取锁,
* 也就是隐士synchronized嵌套,这种称为对象协作间死锁
*/
this.left.sayMyName2();
} catch(Exception e) {
//操作
}
}
public void run() {
this.sayMyName2();
}
}
}
在持有锁的时候调用外部方法要小心死锁问题,因为外部方法可能会获取其他的锁,或者发生阻塞,当一个线程持有锁的时候会阻塞其他尝试获取该锁的线程。