死锁是这样的一种情形:多个同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
【举个例子理解死锁】
张三李四两人去吃饺子,吃饺子需要酱油和醋。
张三抄起了酱油瓶, 李四抄起了醋瓶。
张三:你先把醋瓶给我,我用完了就把酱油瓶给你。
李四:你先把酱油瓶给我,我用完了就把醋瓶给你。
如果这俩人彼此之间互不相让,就构成了死锁。
酱油和醋相当于是两把锁,这两个人就是两个线程。
package Thread;
/**
* @Author : tipper
* @Description : 死锁的情况
*/
public class Demo4 {
//锁1
private static Object locker1 = new Object();
//锁2
private static Object locker2 = new Object();
public static void main(String[] args) {
//线程1
Thread t1 = new Thread(()->{
//加锁
synchronized (locker1) {
//此处的sleep很重要,要确保t1和t2都分别拿到一把锁之后。再进行后续动作。
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2) {
System.out.println("t1 加锁成功!");
}
}
});
//线程2
Thread t2 = new Thread(()->{
//加锁
synchronized (locker2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker1) {
System.out.println("t2 加锁成功!");
}
}
});
}
t1.start();
t2.start();
}
//输出为空且一直在执行
上述代码中,很明显什么都没打印,两个线程都没有获取成功第二把锁。
在此时,死锁代码中,两个 synchronized 是嵌套关系,不是并列关系,嵌套关系说明是在占用一把锁的前提下,获取另一把锁;而并列关系则是先释放前面的锁,再获取下一把锁,这样就不会死锁了。
【哲学家就餐问题】
① 每个哲学家都会做两件事:思考人生,放下筷子 和 拿起左右两侧的两根筷子开始吃面条(先拿起左边,再拿起右边);
② 哲学家什么时候吃面条和思考人生都是随机的;
③ 哲学家吃面条吃多久吃完也是随机的;
④ 哲学家吃面条的过程中,会有左右相邻的哲学家如果也想吃面条,就要阻塞等待。
基于上述规则,通常情况下,整个系统可以良好运转,但是极端情况下会出现问题!
比如,同一时刻,五个哲学家都想吃面条,同时拿起左手的筷子,然后再尝试拿右手的筷子,就会发现右手的筷子都被占用了,哲学家们互不相让,这个时候就形成了死锁。
死锁是一种严重的BUG!会导致程序的线程“卡死”,无法正常工作!
死锁产生的四个必要条件:
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
其中最容易破坏的就是“循环等待”。“互斥使用”、“不可抢占”是锁本身的特性,破坏不了,对于“请求保持”来说,调整代码结构,避免编写“锁嵌套”逻辑。这个方案不一定好用,有的需求可能就是需要进行这种,获取多个锁再操作。
【破坏循环等待】
最常用的一种死锁组织技术就是锁排序。约定加锁的顺序,就可以避免循环等待。
假设有 N 个线程尝试获取 M 把锁,就可以针对 M 把锁进行编号(1,2,3…M)。
N 个线程尝试获取锁的时候,都按照固定的按编号由小到大顺序来获取锁。这样就可以避免环路等待。
比如:哲学家就餐问题,约定每个哲学家都是先拿起编号小的筷子,后拿起编号大的筷子,此时循环等待就破除了,上述系统就不会再出现死锁了。
【可能产生环路等待的代码】
两个线程对于加锁的顺序没有约定,就容易产生环路等待。
package Thread;
public class Demo4 {
//锁1
private static Object locker1 = new Object();
//锁2
private static Object locker2 = new Object();
public static void main(String[] args) {
//线程1
Thread t1 = new Thread(()->{
//加锁
synchronized (locker1) {
//此处的sleep很重要,要确保t1和t2都分别拿到一把锁之后。再进行后续动作。
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
synchronized (locker2) {
System.out.println("t1 加锁成功!");
}
//线程2
Thread t2 = new Thread(()->{
//加锁
synchronized (locker2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
synchronized (locker1) {
System.out.println("t2 加锁成功!");
}
}
}
//运行结果:
此时一直运行
【不会产生环路等待的代码】
约定号先获取 locker1,再获取 locker2,就不会环路等待。
package Thread;
/**
* @Author : tipper
* @Description : 死锁的情况
*/
public class Demo4 {
//锁1
private static Object locker1 = new Object();
//锁2
private static Object locker2 = new Object();
public static void main(String[] args) {
//线程1
Thread t1 = new Thread(()->{
//加锁
synchronized (locker1) {
//此处的sleep很重要,要确保t1和t2都分别拿到一把锁之后。再进行后续动作。
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2) {
System.out.println("t1 加锁成功!");
}
}
});
//线程2
Thread t2 = new Thread(()->{
//加锁
synchronized (locker1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2) {
System.out.println("t2 加锁成功!");
}
}
});
t1.start();
t2.start();
}
}
//运行结果:
t1 加锁成功!
t2 加锁成功!