Java开发之——线程面试篇:死锁和如何避免死锁?


在面试的时候在问起线程锁的部分,经常被问到“什么是死锁”、“怎么避免死锁”之类的问题,甚至开发中在使用锁的时候因为逻辑不严谨导致出现程序无法正确终止或者执行的情况,这些都跟死锁有着不可分割的联系,这篇文章我们就来说说死锁的问题。


1. 什么是死锁

死锁就是当两个或两个以上的线程因竞争相同资源而处于无限期的等待,这样就导致了多个线程的阻塞,出现程序无法正常运行和终止的情况。


举个例子说明下死锁的现象:小明和小张要玩一个玩具,这个玩具有两部分组成必须两部分组装起来才能玩,小明拿了第一部分,小张拿了第二部分;这时候小明等着小张给他第二部分进行组装玩,而小张也等着小明把第一部分给他进行组装玩;两个人都占用着资源谁也不给谁一部分,那么就出现了小明、小张都玩不成的情况;


换成线程中出现死锁解释也是这样,如图:

现在有两个线程A和线程B,线程A通过同步等方式获取了锁1,线程B获取了锁2;在持有锁1的情况下线程A想要再获取锁2,同样的在持有锁2的情况下线程B也想要获取锁1,但是现在锁1和锁2都被线程锁持有并没有被释放,所以就出现了线程A等着线程B释放锁2,线程B等着线程A释放锁1的情况,最终出现两个线程无限期的等待程序无法终止,那么就造成了死锁。


2. 死锁产生的4个必要条件

(1)互斥条件:系统要求对所分配的资源进行排他性控制,即在一段时间内某个资源仅为一个进程所占有(比如:打印机,同一时间只能一个人打印)。此时若有其他进程请求该资源,则请求只能等待,直到有资源释放了位置;

(2)请求和保持条件:进程已经持有了一个资源,但是又要访问一个新的被其他进程占用的资源那么就会阻塞,并且对自己占用的一个资源保持不放;

(3)不剥夺条件:进程对已经获取的资源未使用完之前不能被剥夺,只能使用完之后自己释放。

(4)环路等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。


3. 死锁的常见代码情况

情况一:

//设置两个锁对象smallGate、largeGate
Object smallGate = new Object();
Object largeGate = new Object();

//线程小明

new Thread(()->{


String name = Thread.currentThread().getName();
synchronized (smallGate) {
System.out.println(name+": 把小门锁了,然后休息下");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name+"要进入大门了");
synchronized (largeGate) {
System.out.println("我进不来了");
}
}

},"小明").start();

//线程小张

new Thread(()->{

String name = Thread.currentThread().getName();
synchronized (largeGate) {
System.out.println(name+": 把大门锁了,然后休息下");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name+"要进入小门了");
synchronized (smallGate) {
System.out.println("我进不来了");
}
}

},"小张").start();


运行结果:

分析:这里产生死锁的原因是因线程小明先持有了锁smallGate,然后进行了sleep睡眠之后想要持有锁largeGate;而此时线程小张已经持有了锁largeGate,同样想要持有smallGate,于是出现了资源竞争导致阻塞等待的情况;


情况二:

Object obj1 = new Object();
Object obj2 = new Object();

new Thread(()->{
String name = Thread.currentThread().getName();
synchronized (obj1){
try {
System.out.println(name+ " 等待了,需要被唤醒!");
obj1.wait();
obj2.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

},"t1").start();

new Thread(()->{
String name = Thread.currentThread().getName();
synchronized (obj2){
try {
System.out.println(name+ " 等待了,需要被唤醒!");
obj2.wait();
obj1.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t2").start();


运行结果:

分析:上面死锁的原因是因为线程t1和线程t2分别只持有锁obj1、obj2,但是因为不恰当的通信出现了,线程t1进行了线程等待需要线程t2唤醒,而线程t2也进入了线程等待需要线程t1唤醒,进入了循环等待中导致了死锁产生。


4. 如何避免死锁

(1)保持加锁顺序:当多个线程都需要加相同的几个锁的时候(例如上述情况一的死锁),按照不同的顺序枷锁那么就可能导致死锁产生,所以我们如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。

(2)获取锁添加时限:上述死锁代码情况二就是因为出现了获取锁失败无限等待的情况,如果我们在获取锁的时候进行限时等待,例如wait(1000)或者使用ReentrantLock的tryLock(1,TimeUntil.SECONDS)这样在指定时间内获取锁失败就不等待;

(3)进行死锁检测:我们可以通过一些手段检查代码并预防其出现死锁。


5. 死锁检测

Java中死锁检测手段最多的就是使用JDK带有的jstackJConsole工具了。下面我们以jstack为例来进行死锁的检测;

(1)先运行我们的代码程序

(2)使用JDK的工具JPS查看运行的进程信息,如下:



(3)使用jps查看到的进程ID对其进行jstack 进程分析


分析的结果很长,我们往下找可以看到“Found one Java-level deadlock”,表示程序中发现了一个死锁。

举报
评论 0