目录
死锁
死锁就是同时有多个线程被阻塞,他们中的一个或者全部都在等待某个资源的释放,这样就导致线程被无限期阻塞,整个进程也不会结束。
接下来我们看个例子:
可以看出线程A和线程B都同时持有不同的资源,此时就不会造成阻塞,也不会行程死锁。
如果说此时线程A想要线程B的资源,线程B想要线程A的资源,就会造成下列情况。
此时线程A如果想要线程B的资源,那么就需要等待线程B进行资源的释放,同理,线程B想要线程A的资源,也要等待线程A进行资源的释放。
线程A在尝试获取资源1的时候,此时资源1是被线程B 持有的,线程1就会进行阻塞,线程B在尝试获取资源2的时候,此时资源2是被线程A持有的,所有线程B也会进行阻塞等待。
这个情况就是线程A阻塞等待线程B进行资源释放,但是线程B也在等待线程A进行资源释放,此时就造成了死锁。
我们可以举个简单的例子:
有个老铁在和他的女朋友吃饺子的时候,比如饺子的蘸酱有酱油和醋,此时老铁拿起来醋,老铁女朋友拿起了酱油,此时他们同时有想要对方的蘸酱,老铁给他女朋友说,你把酱油给我,等我把醋和酱油都弄好之后,我在给你,老铁的女朋友也说,你先把醋给我,等我把醋和酱油都弄好之后,我再给你。由于老铁的脾气很犟,于是老铁也就不管了,说那就我们这样等着吧,此时老铁女朋友也脾气犟起来了,说那好,那就我们一直这样等着吧。
于是上述的情况我们可以理解为死锁了。
接下来我们通过代码再进行理解:
public class test {
public static void main(String[] args) {
Object object1 = new Object();
Object object2 = new Object();
Thread t1 = new Thread(()->{
synchronized (object1) {
System.out.println("hello1");
synchronized (object2) {
System.out.println("hello2");
}
}
});
Thread t2 = new Thread(()->{
synchronized (object2) {
System.out.println("hello2");
synchronized (object1) {
System.out.println("hello1");
}
}
});
t1.start();
t2.start();
}
}
在上述代码中我们可以看到,在t1线程和t2线程成功启动之后,于是两个线程开始并发的执行各自的任务,由于线程之间是抢占式执行,执行顺序是随机的。
假如我们此时先看t1线程,在对object1对象加锁之后,然后输出了hello1,此时t2线程也开始执行了,再对object2加锁之后,输出了hello2,此时CPU切换到t1线程上,t1线程要对object2对象进行加速,但是object2是被t2线程已经加锁过,此时这个锁对象还没有被释放,于是t1线程阻塞等待,此时CPU切换到t2线程上,t2线程再对object1对象进行加锁,但是object1是被t1线程加锁中的,所以t2线程也阻塞等待。
此时我们看出,t1线程要想成功加锁object2对象,就得等待t2线程释放object2对象,但是t2线程此时阻塞在等待t1线程释放object1对象,那么t1线程要想释放object1对象,就得先把object2对象加锁,但是t2在持有object2的锁对象的同时在进行阻塞,t1线程也在持有object1的锁对像的同时也在进行阻塞。
这就是一个典型的死锁现象。
接下来我们看看关于死锁的情况。
死锁的情况
1:一个线程一把锁,此时是不会构成死锁的。
因为synchronized是可重入锁,所以即便是我们针对某一个对象连续加锁,也不会构成死锁。
代码实现:
public class test {
public static void main(String[] args) {
Object object = new Object();
Thread t = new Thread(()->{
synchronized (object) {
synchronized (object) {
System.out.println("hello");
}
}
});
t.start();
}
}
因为synchronized在加锁的时候会判定一下,当前申请锁的线程是不是锁的持有者,如果是,那么就直接放行。
2:两个线程两把锁,就会构成死锁
public class test {
public static void main(String[] args) {
Object object1 = new Object();
Object object2 = new Object();
Thread t1 = new Thread(()->{
synchronized (object1) {
System.out.println("hello1");
synchronized (object2) {
System.out.println("hello2");
}
}
});
Thread t2 = new Thread(()->{
synchronized (object2) {
System.out.println("hello2");
synchronized (object1) {
System.out.println("hello1");
}
}
});
t1.start();
t2.start();
}
}
这个代码就是两个线程两把锁,构成了死锁。
3:N个线程,M把锁,线程数和锁的数量更多了,也就根容易构成死锁了。
一个典型的问题,就是哲学家就餐问题:
此时每个哲学家的左右两边都有两根筷子,这个5个哲学家同时的进行吃面条操作和放下筷子的操作。
那么此时比如有个哲学家想拿起筷子进行吃面条操作,但是发现左边或者右边的筷子被另一个哲学家已经拿走了,此时这个哲学家很固执,他也不会放下另一个筷子,而是进行等待。这个哲学家就会阻塞。
如果这5个哲学家同时拿起来左边的筷子,就会发现死锁了!!!
那么什么原因构成的死锁呢 ? 我们接着往下看。
构成死锁的原因
死锁的原因主要有4个:
1:互斥使用 一个线程拿到一把锁之后,另一个线程不能使用(锁的基本特点)
2:不可抢占 一个线程拿到锁,只能自己主动释放,不能被其他线程强行占有。
3:请求和保持 一个线程因为加锁被阻塞时,同时也不会释放自己已经加过的锁。
4:循环等待 上述两个线程两把锁的情况就是循环等待, 可以简单理解为车钥匙锁家里面,家里要是锁车里面了。
上述的4个原因缺一不可,也就是说要想构成死锁,就得同时满足上述4个条件。
解决方案
解决死锁问题的方案其实就是破解上述4的条件的其中任意一个。
其中最容易破坏的就是循环等待。
如何破解循环等待:
我们可以针对锁对象进行锁排序,假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号
(1, 2, 3...M).
N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁. 这样就可以避免循环等待。
破解循环等待:
上述的两个线程两把锁的代码是死锁代码,那么接下来我们破解循环等待来破解死锁。
public class test {
public static void main(String[] args) throws InterruptedException {
Object object1 = new Object();
Object object2 = new Object();
Thread t1 = new Thread(()->{
synchronized (object1) {
System.out.println("hello1");
synchronized (object2) {
System.out.println("hello2");
}
}
});
Thread t2 = new Thread(()->{
synchronized (object1) {
System.out.println("hello1");
synchronized (object2) {
System.out.println("hello2");
}
}
});
t1.start();
t2.start();
}
}
上述代码我们可以发现,每个线程在加锁的时候,因为有多个锁对象,我们给这个多个锁对象从小到大进行排序。此时我们就很好的破解了循环等待,也就解决了死锁的问题。