目录
引言
- 一旦程序出现死锁,就会导致线程无法继续执行后续工作,意味着该程序有严重 bug
- 死锁是非常隐蔽的,在开发阶段,不经意间就会写出死锁代码且不容易测试出来
死锁原因
情况一
- 一个线程一把锁,连续加锁两次,如果锁是不可重入锁,就会死锁
- Java 中 synchronized 和 ReentrantLock 都是可重入锁
可点击下方链接简单了解可重入锁和不可重入锁的区别
情况二
- 两个线程两把锁,t1 和 t2 各自针对 锁A 和 锁B 加锁,再尝试获取对方的锁
代码实例
public class ThreadDemo15 { public static void main(String[] args) { Object locker1 =new Object(); Object locker2 =new Object(); Thread t1 = new Thread(() -> { synchronized (locker1) { System.out.println("线程t1 拿到锁 locker1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程t1 拿到锁 locker1 后尝试获取锁 locker2"); synchronized (locker2){ System.out.println("线程t1 拿到锁 locker2"); } } }); Thread t2 = new Thread(() -> { synchronized (locker2) { System.out.println("线程t2 拿到锁 locker2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程t2 拿到锁 locker2 后尝试获取锁 locker1"); synchronized (locker1){ System.out.println("线程t2 拿到锁 locker1"); } } }); t1.start(); t2.start(); } }
运行结果:
- 线程t1 未拿到锁 locker2,线程t2 未拿到锁 locker1
具体解释
- 线程t1 获取到锁 locker1 ,线程t2 获取到锁 locker2
- 但是当线程t1 想要在自身获取到锁 locker1 的基础上再获取锁 locker2 时
- 因为线程t2 已经先将锁 locker2 获取走了,此时线程t1 想要获取锁 locker2 时,需等待到线程t2 将锁 locker2 释放
- 但是又因为线程t2 将锁 locker2 释放的前提是能够获取到锁 locker1,而锁 locker1 又被线程t1 获取走了
- 所以线程t2 要获取到锁 locker1 就需要等待线程t1 释放锁 locker1
- 从而陷入死循环中,造成死锁,线程t1 和线程t2 为阻塞状态
情况三
- 多个线程多把锁(情况二为情况三的特殊情况)
经典案例
- 哲学家就餐问题
- 圆桌的东南西北各坐一位哲学家,哲学家左右两边各放一根筷子
- 哲学家想吃面条就需要同时拿起左右两边的筷子
- 上图为极端情况
- 同一时刻,所以的哲学家同时拿起左手的筷子,此时所有的哲学家都拿不起右手的筷子,都需要等待右边的哲学家把筷子放下
- 此时的局面就僵住了,所有哲学家都进行阻塞等待,从而出现了死锁的情况
使用 jconsole 定位死锁
关于 jconsole 的详细使用可以点击下方链接跳转
- 此处我们使用 jconsole 查看情况二的死锁状况
死锁四个必要条件
互斥使用
- 线程A 拿到锁 locker ,如果线程B 也想要拿到锁 locker,则需阻塞等待
不可抢占
- 线程A 拿到锁 locker 后,必须是线程A 主动释放锁 locker,不能是线程B 强行获取锁 locker
请求和保持
- 线程A 拿到锁 locker1,其再次尝试获取锁 locker2,此时锁 locker1 还是会继续保持
- 不能因为线程A 获取锁 locker2,而把锁 locker1 给释放了
循环等待
- 线程A 尝试获取到锁 locker1 和锁 locker2,线程B 尝试获取锁 locker2 和锁 locker1
- 线程A 在获取锁 locker2 的时候等待线程B 释放锁 locker2
- 同时线程B 在获取锁 locker1 的时候等待线程B 释放锁 locker1
注意:
- 这四个条件同时出现,才会出现死锁
- 前三个条件都是锁的基本特性,程序员是无法控制的
- 仅循环等待是这四个条件中唯一一个与代码结构相关,也是程序员可以控制的条件
死锁解决方法
- 我们可以通过打破死锁的必要条件来避免死锁,同时唯一的突破口为 循环等待
基本方法
- 给锁进行编号,约定有多个锁的时候,必须先拿编号小的锁,后拿编号大的锁
具体思路
- 还是当所有哲学家同时向拿起筷子吃面时,所有哲学家都先拿编号小的筷子
- 此时 上方哲学家 也会先拿1号筷子
- 但是发现1号筷子已经被 右方哲学家 拿走了,所以 上方哲学家 就会阻塞等待 右方哲学家 释放1号筷子
- 但是因为 上方哲学家 并未拿4号筷子,所有 左方哲学家 能够顺利的拿到3号和4号筷子,同时进行吃面的动作,吃完面便释放3号和4号筷子
- 紧接着 下方哲学家 便能拿到 左方哲学家 释放的3号筷子,同时拿起2号和3号筷子,进行吃面的动作,最后释放2号和3号筷子
- 右方哲学家同理,拿到 下方哲学家 释放的2号筷子,完成吃面动作,再释放1号和2号筷子
- 上方哲学家 便能顺利拿到 右方哲学家释放的1号筷子,同时再拿起4号筷子,完成吃面的动作
- 通过对筷子进行编号,且规定先拿编号小的再拿编号大的筷子,就能很好的避免死锁的发生
解决情况二死锁问题
public class ThreadDemo15 { public static void main(String[] args) { Object locker1 =new Object(); Object locker2 =new Object(); Thread t1 = new Thread(() -> { synchronized (locker1) { System.out.println("线程t1 拿到锁 locker1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程t1 拿到锁 locker1 后尝试获取锁 locker2"); synchronized (locker2){ System.out.println("线程t1 拿到锁 locker2"); } } }); Thread t2 = new Thread(() -> { synchronized (locker1) { System.out.println("线程t2 拿到锁 locker1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程t2 拿到锁 locker1 后尝试获取锁 locker2"); synchronized (locker2){ System.out.println("线程t2 拿到锁 locker2"); } } }); t1.start(); t2.start(); } }
运行结果: