前言
死锁是并发编程中非常重要的一个知识点,死锁使程序运行无法得到正确的结果同时降低操作系统的资源利用率,危害非常大。本篇文章以哲学家就餐问题作为引导,讲解产生死锁的条件以及如何避免死锁,供参考学习。
一、哲学家就餐问题
五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭或者思考。
1.哲学家围在⼀起先思考,思考中途饿了就会想吃饭。
2.每两个哲学家之间有一只餐叉。哲学家必须用两只餐叉吃东西,并且他们只能使用自己左右手边的那两只餐叉,吃完的时候会把叉子放回原处,继续思考。
哲学家从来不交谈,这就很危险,可能产生死锁,如每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反),这样他们几个就搁这卡bug了。
二、造成死锁的条件
我们为了防止多线程竞争共享资源而导致数据错乱,都会在操作共享资源之前加上锁,只有成功获得到锁的线程,才能操作共享资源,获取不到锁的线程就只能等待,直到锁被释放。
在哲学家就餐问题中,涉及到了多个线程(哲学家)竞争共享资源(叉子),如果不加以控制可能就会出现死锁的情况。
造成死锁的四个条件:
- ⼀个资源每次只能被⼀个线程使用(一个叉子只能被一个哲学家使用)
- ⼀个线程在阻塞等待某个资源时,不释放已占有资源 (即使拿不到第二个叉子,哲学家也不会放弃已经拿到的叉子)
- ⼀个线程已经获得的资源,在未使用完之前,不能被强姓剥夺 (不能从其他哲学家手上抢走叉子)
- 若干线程形成头尾相接的循环等待资源关系(每个哲学家都在等待下一个人释放叉子,形成了一个环)
这是造成死锁必须要达到的4个条件!如果要避免死锁,只需要不满⾜其中某⼀个条件即可。但是其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
下面就来用一段代码演示一下死锁的产生情况:
public class DieSynchronizedThread extends Thread{
static Object A=new Object();
static Object B=new Object();
boolean flag;
@Override
public void run() {
if(flag){
synchronized (A){
System.out.println(Thread.currentThread().getName()+"拿到了A");
synchronized (B){
System.out.println(Thread.currentThread().getName()+"拿到了B");
}
}
}else{
synchronized (B){
System.out.println(Thread.currentThread().getName()+"拿到了B");
synchronized (A){
System.out.println(Thread.currentThread().getName()+"拿到了A");
}
}
}
}
public static void main(String[] args) {
DieSynchronizedThread t1=new DieSynchronizedThread();
DieSynchronizedThread t2=new DieSynchronizedThread();
t1.setName("小黑");
t2.setName("小白");
t1.flag=true;
t2.flag=false;
t1.start();
t2.start();
}
}
小白和小黑都需要拿到A和B才能完成自己的操作,但由于没有对获取资源进行控制,小黑先去拿了A,小白先去拿了B,导致它们两个卡了bug,出现了死锁。
三、如何避免死锁
上文说了要打破死锁,就需要打破第4个条件,即不出现循环等待锁的关系。打破这个条件最常用的方法就是资源有序分配法,即两个或多个线程以相同的顺序去申请自己需要的资源。下面将上面的代码修改一下,两个线程都先去获取A,再去获取B。
if(flag){
synchronized (A){
System.out.println(Thread.currentThread().getName()+"拿到了A");
synchronized (B){
System.out.println(Thread.currentThread().getName()+"拿到了B");
}
}
}else{
synchronized (A){
System.out.println(Thread.currentThread().getName()+"拿到了A");
synchronized (B){
System.out.println(Thread.currentThread().getName()+"拿到了B");
}
}
}
这样它们之间就可以进行愉快的玩耍了,因为只有获取了A才能去获取B,不会出现拿到A,B被别人抢了的情况。
作为程序员,在开发过程中:
- 要注意加锁顺序,保证每个线程按同样的顺序进⾏加锁
- 要注意加锁时限,可以针对所设置⼀个超时时间
- 要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进⾏解决
总结
简单来说,死锁问题的产生是由两个或者以上线程并行执行的时候,争夺资源而互相等待造成的。死锁有四个条件,缺一不可,而打破死锁需要打破第四个条件,最常用的方法就是资源有序分配法。