浅谈Java中死锁问题
1- Java中死锁定义
在Java中synchronized关键字修饰的方法或者其他通过Lock加锁方式修饰方法、代码块可以防止别的任务在还没有释放锁的时候就访问这个对象!如果一个任务在等待另一个任务持有的锁,而后者又去等待其他任务持有的锁,这样一直下去,直到这个任务等待第一个任务持有的锁,这样就形成一个任务之间相互等待的连续循环,没有哪个任务能够继续执行,此时所有任务停止,这种情况就是死锁!
2- 死锁实例
2.1-实例业务场景
本例来自Thinking in Java,具体的业务场景如下:
有5个哲学家去就餐,但是就只有5根筷子,这5个哲学家围坐在一起,每个哲学家的左边和右边都有一根筷子。哲学家可能在思考,也可能在就餐。哲学家要就餐的话必须获取到左边和右边两根筷子,如果这个哲学家的左边或者右边的筷子已经正在被其他哲学家使用,则要就餐的哲学家必须等待其他哲学家就餐完毕释放筷子!具体见下图:
![](https://i-blog.csdnimg.cn/blog_migrate/41e83c868050c7a0d4c77de680d72c5d.png)
图中圆圈表示哲学家,线条表示筷子!
2-2实现代码
在本例中筷子属于竞争资源,加锁的方法肯定在筷子对应的类中,定义chopstick类,表示筷子,代码如下:
package thread.test.deadLock;
/**
* 筷子类
*/
public class Chopstick {
//筷子的使用状态,true-使用,false-未使用
private Boolean taken = false;
/**
* 使用筷子的方法
*
* @throws InterruptedException
*/
public synchronized void take() throws InterruptedException {
//如果筷子的状态是使用,则等待
while (taken) {
wait();
}
//将筷子的状态改成使用
taken = true;
}
/**
* 放下筷子方法
*/
public synchronized void drop() {
//将筷子的状态改成未使用
taken = false;
//通知其他哲学家可以使用这根筷子
notifyAll();
}
}
在本例中哲学家属于任务,哲学家类实现Runnable接口使用筷子即竞争资源,在run方法中哲学家先思考片刻,然后先拿右边的筷子,后拿左边的筷子,如果都拿到了那么哲学家开始吃饭,然后先释放右边的筷子再释放左边的筷子!如果不能拿到左右两根筷子,那么哲学家持有一根筷子,等待另一根筷子被其他哲学家释放!代码如下:
package thread.test.deadLock;
import java.util.concurrent.TimeUnit;
/**
* 哲学家类
*/
public class Philosopher implements Runnable {
//哲学家左边的筷子
private Chopstick left;
//哲学家右边的筷子
private Chopstick right;
//哲学家编号
private final int id;
//哲学家思考
private final int ponderFactor;
public Philosopher(int id, int ponderFactor, Chopstick left, Chopstick right) {
this.id = id;
this.ponderFactor = ponderFactor;
this.left = left;
this.right = right;
}
/**
* 哲学家思考,思考时间ponderFactor
*
* @throws InterruptedException
*/
private void thinking() throws InterruptedException {
//如果不思考直接跳过
if (ponderFactor == 0) {
return;
}
//哲学家思考ponderFactor
TimeUnit.MILLISECONDS.sleep(ponderFactor);
}
/**
* run方法,提交的任务如果不打断的话且没有死锁的话会一直进行下去
*/
@Override
public void run() {
try {
while (!Thread.interrupted()) {
//哲学家先思考片刻
System.out.println(this + " is thinking!");
thinking();
//拿右边的筷子
System.out.println(this + " taking right chopstick!");
right.take();
System.out.println(this + " taked right chopstick!");
//拿左边的筷子
System.out.println(this + " taking left chopstick!");
left.take();
System.out.println(this + " taked left chopstick!");
//吃饭
System.out.println(this + " is eating!");
//放下筷子
right.drop();
left.drop();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public String toString() {
return "Philosopher" + id;
}
}
显然,定义的Philosopher很容易造成死锁,如果每个哲学家都是先拿右手边的筷子或者先拿左手边的筷子,那么5个哲学家都是持有一个筷子并等待其他哲学家释放筷子!此时,每个任务就会持有其他任务等待的锁,形成一个相互等待的连续循环,从而造成死锁!
下面演示下死锁的示例代码,代码如下:
package thread.test.deadLock;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 死锁问题测试
*/
public class PhilosopherDeadLockTest {
public static void main(String... args) {
//创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
//创建5根筷子对象
Chopstick[] chopsticks = new Chopstick[5];
for (int i = 0; i < 5; i++) {
chopsticks[i] = new Chopstick();
}
//提交
for (int i = 0; i < 5; i++) {
executorService.submit(new Philosopher(i, 5, chopsticks[i], chopsticks[(i + 1) % 5]));
}
executorService.shutdown();
}
}
运行下,打印结果如下:
由上图可知,程序处于阻塞状态,打印了5条Philosopher* taked right chopstick,表示这5个任务都先拿到了右边的筷子,打印了5条Philosopher* taking left chopstick,大家都没有拿到左边的筷子,等处于等待的状态!
![](https://i-blog.csdnimg.cn/blog_migrate/ad28284d268e3efc8990bd4aa27c9a82.png)
由上图可知,程序处于阻塞状态,打印了5条Philosopher* taked right chopstick,表示这5个任务都先拿到了右边的筷子,打印了5条Philosopher* taking left chopstick,大家都没有拿到左边的筷子,等处于等待的状态!
3- 造成死锁的原因
造成死锁的原因有如下4条,且必须同时满足:
1-互斥条件:任务使用的资源中至少有一个是不能共享的,本例中筷子都不能共享,需要竞争,筷子的使用和释放方法都是用了synchronized关键字修饰!
2-至少有一个任务它必须持有一个资源且正在等待获取一个当前正在被别的任务持有的资源,本例中哲学家必须持有一根筷子且其他筷子都被其他哲学家持有!
3-资源不能被任务抢占,任务必须把资源释放当做普通事件,资源只能被占用的资源释放后才能被其他任务获取到!
4-必须有循环等待,这时一个任务等待其他任务释放资源,其他任务又在等待另外一个任务释放资源,且直到最后有一个任务在等待第一个任务释放资源,使得大家都被锁住!
4- 解锁死锁
知道了造成死锁的原因了,那么解决死锁问题就是让代码逻辑不会同时满足上述4条即可!
在本例中,可以让前4个哲学家先获取右边的筷子然后获取左边的筷子,让最后一个哲学家先获取左边的筷子然后获取右边的筷子,这样第5个哲学家在获取左边的筷子的时候就会被阻塞,此时有一个筷子处于未被占用的状态,第1个哲学家就可以获取到,吃完后就会释放左右两根筷子,从而不会造成死锁问题!这是通过打破上述造成死锁原因第4条的方式解决死锁问题,具体代码如下:
package thread.test.deadLock;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 解决死锁问题测试
*/
public class PhilosopherFixedDeadLockTest {
public static void main(String... args) {
//创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
//创建5根筷子对象
Chopstick[] chopsticks = new Chopstick[5];
for (int i = 0; i < 5; i++) {
chopsticks[i] = new Chopstick();
}
//提交
/**
* 解决死锁:
* 前4个哲学家先拿右边筷子,后拿左边筷子
* 最后一个哲学家先拿左边筷子,后拿右边筷子
*/
for (int i = 0; i < 5; i++) {
if (i < 4) {
executorService.submit(new Philosopher(i, 5, chopsticks[i], chopsticks[(i + 1) % 5]));
} else {
executorService.submit(new Philosopher(i, 5, chopsticks[0], chopsticks[5]));
}
}
executorService.shutdown();
}
}
运行发现可以很流畅的打印筷子的使用与释放,不会有死锁问题产生!
5- 总结
Java对死锁没有提供语言层面上的支持,只能通过程序员通过仔细的设计来避免死锁!所以在设计多线程程序时,应该考虑造成死锁的4个条件,绝对不能让上述4个条件同时满足!