死锁详解
一、什么是死锁
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象,若无外力作用,他们都将无法进行下去。也就是说两个线程在拥有锁的情况下,又尝试获取对方的锁,从而导致程序进入阻塞状态。
手撕死锁代码(面试常考)~
import java.util.concurrent.TimeUnit;
/**
* 死锁示例
*/
public class ThreadDemo17 {
public static void main(String[] args) {
Object lockA=new Object();
Object lockB=new Object();
Thread t1=new Thread(()->{
//1.占有一把锁(锁A)
synchronized (lockA){
System.out.println("线程1:获取锁A");
//休眠1s(让线程2有时间获取锁B)
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.试图获取另一个线程的锁B
synchronized (lockB){
System.out.println("线程1:获取锁B");
}
}
});
t1.start();
Thread t2=new Thread(()->{
//1.占有一把锁(锁B)
synchronized (lockB){
System.out.println("线程2:获取锁B");
//休眠1s(让线程2有时间获取锁B)
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.试图获取另一个线程的锁A
synchronized (lockA){
System.out.println("线程2:获取锁A");
}
}
});
t2.start();
}
}
怎样查看死锁呢?我们可以使用jvsualvm/jmc/jconsole查看死锁,下面我们展示一下用jvsualvm查看死锁。
我们可以看到Thread0和Thread1有死锁出现。
二、死锁产生的必要因素
1. 互斥条件
一个资源同一时间只能被一个线程占用。
2.不可剥夺条件
一个资源被占用之后,如果不是拥有资源的线程释放,那么其他线程得不到此资源。
3.请求并持有条件
当一个线程拥有了某个资源之后,还不满足,又在请求其他资源。
4.环路等待条件
多个线程在请求资源的情况下,形成了环路链。
以上四个条件是导致死锁的必要条件,要形成死锁,以上4个条件缺一不可。
三、解决死锁
3.1 解决死锁方案分析
打破形成死锁的一个或者是多个条件即可。
接下来我们来分析一下,哪些条件是可以打破的,哪些不行?
- 互斥条件:系统特性,不可打破。
- 不可剥夺条件:系统特性,不可打破。
- 请求并持有条件:可以打破。
- 环路等待条件:可以打破。
通过上述分析,我们可以通过打破环路等待条件和请求并持有条件来解决死锁问题。
3.2 解决死锁方案1:顺序锁
所谓顺序锁就是有顺序的获取锁,从而避免环路等待条件,解决死锁问题。如下图所示:
我们让线程1和线程2获取锁的顺序统一,也就是让线程1和线程2同时执行,先获取锁A,但是只能有一个线程可以获取到锁A,没有获取到锁A的线程等待获取锁A,获取到锁A的线程继续执行获取锁B,因为没有线程争抢和拥有锁B,所以拥有锁A的线程会顺利获取到锁B,之后执行相应的业务代码,再将锁资源全部释放(先释放锁B,再释放锁A),然后另一个等待获取锁A的线程就可以顺利得到锁资源,执行后续代码,这样就解决了死锁问题。
讲清楚了解决方案,我们上代码!
import java.util.concurrent.TimeUnit;
/**
* 解决死锁方案:破环环路等待条件
*/
public class UnDead {
public static void main(String[] args) {
Object lockA=new Object();
Object lockB=new Object();
Thread t1=new Thread(()->{
synchronized (lockA){
System.out.println("线程1:获取锁A");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println("线程1:获取锁B");
//业务代码
System.out.println("线程1:释放锁B");
}
System.out.println("线程1:释放锁A");
}
});
t1.start();
Thread t2=new Thread(()->{
synchronized (lockA){
System.out.println("线程2:获取锁A");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println("线程2:获取锁B");
//业务代码
System.out.println("线程1:释放锁B");
}
System.out.println("线程1:释放锁A");
}
});
t2.start();
}
}
3.3 解决死锁的方案3:轮询锁
轮询锁是通过打破请求并持有条件来解决死锁问题的,实现思路就是通过轮询来尝试获取锁,如果有一个锁获取失败,则释放当前线程拥有的所有锁,等待下一轮再尝试获取锁。
上代码~~
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 解决死锁方案2:通过轮询锁破坏请求并持有条件
*/
public class UnDead2 {
public static void main(String[] args) {
Lock lockA=new ReentrantLock();
Lock lockB=new ReentrantLock();
//创建线程1(使用轮询锁)
Thread t1=new Thread(()->{
try {
pollingLock(lockA,lockB);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
//创建线程2
Thread t2=new Thread(()->{
lockB.lock();
System.out.println("线程2获取到锁B");
try {
Thread.sleep(1000);
System.out.println("线程2等待获取锁A...");
lockA.lock();
try {
System.out.println("线程2获取到锁A");
}finally {
lockA.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lockB.unlock();
}
});
t2.start();
}
/**
* 轮询锁
* @param lockA
* @param lockB
*/
private static void pollingLock(Lock lockA, Lock lockB) throws InterruptedException {
while(true){
if(lockA.tryLock()){//尝试获取锁A
System.out.println("线程1获取到锁A");
try {
Thread.sleep(1000);
System.out.println("线程1等待获取锁B...");
if (lockB.tryLock()) {
try {
System.out.println("线程1获取到锁B");
}finally {
lockB.unlock();
System.out.println("线程1释放锁B");
break;
}
}
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lockA.unlock();
System.out.println("线程1释放锁A");
}
}
//等待1s继续执行
Thread.sleep(1000);
}
}
}
以上就是死锁的产生以及解决方案啦,如果觉得对你有帮助就点个赞支持一下吧~