❣️关注专栏: JavaEE
死锁
- ☘️1.什么是死锁
- ☘️2.死锁的三个典型情况
- ☘️2.1情况一
- ☘️2.2情况二
- ☘️2.2.1死锁的代码展示
- ☘️2.3多个线程多把锁
- ☘️3死锁产生的必要条件
- ☘️3.1互斥性
- ☘️3.2不可抢占
- ☘️3.3请求和保持
- ☘️3.4循环等待
- ☘️4如何避免死锁
- ☘️4.1避免死锁代码
☘️1.什么是死锁
死锁是一个非常让程序猿烦恼的问题,一旦所写的程序有了死锁,那么程序就无法执行下去,会出现严重的 bug,并且死锁非常隐蔽,我们不会轻易发现它,在开发阶段,不经意期间我们就会写出死锁,很难检测出来。
那么什么是死锁呢?竟然让我们如此烦恼。
“死锁”就是2个或2个以上的线程互相持有对方想要的资源,导致各自处于阻塞等待状态,致使程序无法执行下去,这就是“死锁”。
☘️2.死锁的三个典型情况
☘️2.1情况一
一个线程一把锁,连续加两次。如果锁是不可重入锁,就会死锁。
Java 里的 synchronized 和 ReentrantLock 都是可重入锁
☘️2.2情况二
两个线程两把锁,t1 和 t2 各自对 锁A 和 锁B 加锁,再尝试获取对方的锁。线程在竞争资源,导致死锁。
下面图解展示以下这种情况:
☘️2.2.1死锁的代码展示
/**
* 死锁
* 两个线程两把锁,一个线程各对应一把锁,再获取另一个锁
*/
public static void main(String[] args) {
Object jiangyou = new Object();
Object cu = new Object();
Thread t1 = new Thread(() -> {
synchronized (jiangyou) {
// 加入 sleep 都是为了让线程先把对应的第一个锁拿到
// 加上休眠,表示让 jiangyou 的锁获取到之后,再获取 cu 的锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (cu) {
System.out.println("t1把酱油和醋都拿到了");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (cu) {
// 加上休眠,表示让 cu 的锁获取到之后,再获取 jiangyou 的锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (jiangyou) {
System.out.println("t2把醋和酱油都拿到了");
}
}
});
t1.start();
t2.start();
}
运行之后发现无法打印出结果,那么我们查看一下线程调用栈,按照这个路径查找到线程调用栈,我这里的jdk版本如下是jdk1.8.0_192,不同的版本名称不一样,你是 jdk1.9.0就点开 jdk1.9.0就行了。
点击jconsole.exe,按照以下步骤进行连接:有人可能不会出现第一步(第一步的这个名称是我 idea 中这个代码的类名),因为你可能没有运行该代码,你要保持这个代码一直在运行,不要停止运行,就会出现这个连接。
同样点开 Thread-1 也会有这样的描述。
针对这样的死锁问题,需要借用这个 jconsole.exe 工具来帮助我们定位到死锁的地方。
☘️2.3多个线程多把锁
我们在学校学习的时候,最经典的就是哲学家就餐问题。一共有5个哲学家(A、B、C、D、E),一共有5根筷子(1、2、3、4、5),共同吃一碗面条。
每个哲学家有两种状态:
(1)思考问题(相当于线程中的阻塞状态)
(2)拿起筷子吃面条(相当于线程获取到锁之后执行一些计算)
由于操作系统是随时调度的,所以这五个哲学家,随时都可能想要吃面条,也随时都可能思考问题。正常来说,如果想要吃面条,需要拿起左手和右手的两根筷子。
假设此时出现了特殊情况:同一时刻,所有的哲学家都拿起了左手的筷子,需要吃面还需要拿起右手的筷子,就需要等待右边的哲学家把筷子放下,此时就是出现了死锁。
☘️3死锁产生的必要条件
☘️3.1互斥性
线程1 拿到了锁,线程2 就必须等着,所以线程之间是互斥使用锁的。
☘️3.2不可抢占
线程1 拿到了锁,必须是 线程1 主动释放,线程2 才能得到锁,如果 线程1 不主动释放锁,线程2 是不能强行获取到锁的。
☘️3.3请求和保持
线程1 拿到 锁A 之后,再尝试获取 锁B,A 这把锁还是保持的,不会因为 线程1 想要获取 锁B 就把 锁A 释放了。
☘️3.4循环等待
线程1 拿到 锁A 之后,尝试获取 锁B,线程2 拿到 锁B 之后,尝试获取锁A。
线程1 在获取 锁B 的时候等待 线程2 释放 B,同时线程2 在获取 A 的时候需要等待线程1 释放 A。
这4个必要条件缺一不可。前3点都是锁的基本特性,一般不需要我们程序猿自己去设置,只有第4个是唯一一个和代码相关的,也是程序猿可以控制的。
☘️4如何避免死锁
想要避免死锁,我们就必须打破必要条件,这里的突破口就是第四点的循环等待这一条件,我们可以给锁进行编号,然后制定一个固定的顺序(比如从小到大)来加锁。任意个线程加多把锁的时候都让线程按照上边的规则,就是按照固定的顺序加锁,自然循环等待的条件就会被打破,此时就会避免死锁了。
☘️4.1避免死锁代码
/**
* 解决死锁:指定一个固定的顺序
* 比如:都先拿 jiangyou 再拿 cu
*/
public static void main1(String[] args) {
Object jiangyou = new Object();
Object cu = new Object();
Thread t1 = new Thread(() -> {
synchronized (jiangyou) {
// 加入 sleep 都是为了让线程先把对应的第一个锁拿到
// 加上休眠,表示让 jiangyou 的锁获取到之后,再获取 cu 的锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (cu) {
System.out.println("t1把酱油和醋都拿到了");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (jiangyou) {
// 加上休眠,表示让 cu 的锁获取到之后,再获取 jiangyou 的锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (cu) {
System.out.println("t2把醋和酱油都拿到了");
}
}
});
t1.start();
t2.start();
}
因为指定了一个顺序:都先拿 jiangyou(酱油),再拿 cu(醋)。让线程 t1 把酱油和醋拿到之后就会释放锁,然后线程 t2 就会去获取,从而避免了循环等待对方了。运行结果如下: