什么是死锁?
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。
一个线程一把锁,这个就是之前所介绍的 可重入锁
线程A 针对锁1 连续加锁两次,就构成了死锁 !
第一次加锁 加锁成功,第二次加锁 就需要第一次的锁释放,于是就阻塞等待;
但是,第一次加锁释放,就得依赖第二次加锁成功;
于是,"死锁" 就出现了!!!
此时,不管是不是可重入锁,都会造成 "死锁" 问题 !
可以用一个例子来说明情况:
假设 甲同学 和 乙同学 在一起
甲同学 吃饺子喜欢蘸酱油,乙同学 吃饺子喜欢蘸醋,由于在一起了,生活习性都受到了彼此的影响,两位同学吃饺子的时候 喜欢都蘸上一点了
有一天,吃饺子的时候,甲同学拿起了酱油,乙同学拿起了醋 ~
甲同学说:"你把醋给我,我用完了都给你";乙同学说:"你把酱油给我,我用完了就把醋给你" ~
此时,两者相持不下,这就造成了 "死锁" 问题
此时,甲同学 和 乙同学 就可以看成是两个线程,酱油和醋 就可以看成是两把锁
线程1 获取到锁A,线程2 获取到锁B;
线程1 尝试获取锁B(需要线程2 释放锁B) ,线程2 尝试获取到锁A(需要线程1 释放锁A);
在这种情况下,逻辑上就构成了循环,就构成了 "死锁" ~
- import java.util.concurrent.TimeUnit;
-
- public class Main {
- public static void main(String[] args) {
- Object lock1 = new Object();
- Object lock2 = new Object();
- Thread t1 = new Thread(() -> {
- synchronized (lock1) {
- System.out.println( "t1:已经获取到锁1");
- try {
- Thread.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- synchronized (lock2) {
- System.out.println( "t1:获取到锁2");
- }
- }
- }, "t1");
- t1.start();
-
- Thread t2 = new Thread(() -> {
- synchronized (lock2) {
- System.out.println( "t2:获取到锁2");
- try {
- Thread.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- synchronized (lock1) {
- System.out.println( "t2:获取到锁1");
- }
- }
- }, "t2");
- t2.start();
- }
- }
一直等待,死锁了
此时,这种情况和 第二种情况 类似,只是更复杂一点而已
在多个线程多把锁的情况下,"死锁"问题 就是一个概率性的问题,但是也绝对不能忽视 !
在谈到 "多个线程多把锁" 的时候,就会引出一个很经典的问题 —— "哲学家就餐问题" !
故事背景:
有 5 个哲学家,相当于有 5 个线程,他们只会做两件事情:
思考人生(相当于是 线程休眠)
吃面条(相当于是 在CPU上运行)
由于多线程的调度是无序的,所以说 这几个哲学家 什么时候去思考人生,什么时候去吃面条,我们是不确定的但是,此时 在餐桌上 一共只有 5 根筷子,并且 这5 根筷子 分别在 两两哲学家 之间
并且,他们之间都不相互嫌弃,吃面条的时候要拿起 左右手两双筷子(这就导致相邻的哲学家需要等待) ~
此处的筷子就视为 两把"锁",只有这两把锁都获取到了,才可以吃面条 ~
出现 "死锁" 问题的情况:
假设 在同一时刻,所有的哲学家都想吃面条 ~
他们同时伸出左手,拿起左边的筷子;然后又同时伸出右手,尝试去拿右边的筷子;此时,右手的筷子都拿不起来,因此都无法吃面
由于哲学家都非常固执,导致即使吃不到面条 也不会放下左手的筷子,这样的情况就会一直持续下去 ,这就构成了死锁
鉴于 "死锁"问题,程序员大佬们 总结了 4 个 "死锁"产生的必要条件:
根据 产生 "死锁" 的必要条件,我们可以知道,前三个必要条件 都是在描述锁的基本特点,在实际情况下 我们并不好直接去破坏
但是,第四个必要条件 却是和代码编写密切相关
如果我们能够在编码上做出一些注意和约定,就可以打破 "循环等待",避免死锁 !!!
针对两个特定的锁,开发者可以尝试按照锁对象的hashCode值大小的顺序,分别获得两个锁,这样锁总是会以特定的顺序获得锁,那么死锁也不会发生。问题变得更加复杂一些,如果此时有多个线程,都在竞争不同的锁,简单按照锁对象的hashCode进行排序(单纯按照hashCode顺序排序会出现“环路等待”),可能就无法满足要求了,这个时候开发者可以使用银行家算法,所有的锁都按照特定的顺序获取,同样可以防止死锁的发生。
针对多把锁,进行编号:1、2、3、4、......
并且约定在获取多把锁的时候,要明确获取锁的顺序是 从小到大(或者 从大到小) 的顺序
如(此处以从小到大为例):
线程要拿到 1、2 这两把锁,就先获取 1,再获取 2;
线程要拿到 2、4 这两把锁,就先获取 2,再获取 4 ~
只要所有的线程都遵循这个顺序,就不会出现 "循环等待",就不会出现死锁 !!!
我们可以把这个解决办法 带入到上面的 "哲学家就餐问题" 看看 ~
约定:获取所得顺序是:从小到大 ~
之后,最左边的哲学家 就可以得到两把锁了,于是就可以吃到面条了 ~
等到 吃完面条之后,会释放 4、5两把筷子;然后 最上面的哲学家 也可以吃到面条了,......,就这样的话,顺时针旋转,依次 5 位哲学家都可以吃上面条了
当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。
当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:
1、Jstack命令
jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。 Jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
2、JConsole工具
Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。