2023.10.16 关于 死锁 详解

目录

引言

死锁原因

情况一

情况二

情况三

使用 jconsole 定位死锁

死锁四个必要条件

互斥使用

不可抢占

请求和保持

循环等待

 死锁解决方法

解决情况二死锁问题


引言

  • 一旦程序出现死锁,就会导致线程无法继续执行后续工作,意味着该程序有严重 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();
    }
}

运行结果:

2023.10.16 关于 死锁 详解_第1张图片

  • 线程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 为阻塞状态

情况三

  • 多个线程多把锁(情况二为情况三的特殊情况)

经典案例

  • 哲学家就餐问题

2023.10.16 关于 死锁 详解_第2张图片

  • 圆桌的东南西北各坐一位哲学家,哲学家左右两边各放一根筷子
  • 哲学家想吃面条就需要同时拿起左右两边的筷子

2023.10.16 关于 死锁 详解_第3张图片

  • 上图为极端情况
  • 同一时刻,所以的哲学家同时拿起左手的筷子,此时所有的哲学家都拿不起右手的筷子,都需要等待右边的哲学家把筷子放下
  • 此时的局面就僵住了,所有哲学家都进行阻塞等待,从而出现了死锁的情况

使用 jconsole 定位死锁


关于 jconsole 的详细使用可以点击下方链接跳转

jconsole 详细使用步骤


  •  此处我们使用 jconsole 查看情况二的死锁状况

2023.10.16 关于 死锁 详解_第4张图片

死锁四个必要条件

互斥使用

  • 线程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

注意:

  • 这四个条件同时出现,才会出现死锁
  • 前三个条件都是锁的基本特性,程序员是无法控制的
  • 仅循环等待是这四个条件中唯一一个与代码结构相关,也是程序员可以控制的条件

 死锁解决方法

  • 我们可以通过打破死锁的必要条件来避免死锁,同时唯一的突破口为 循环等待 

基本方法

  • 给锁进行编号,约定有多个锁的时候,必须先拿编号小的锁,后拿编号大的锁

2023.10.16 关于 死锁 详解_第5张图片

具体思路

  • 还是当所有哲学家同时向拿起筷子吃面时所有哲学家都先拿编号小的筷子
  • 此时 上方哲学家 也会先拿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();
    }
}

运行结果:

2023.10.16 关于 死锁 详解_第6张图片

你可能感兴趣的:(多线程,java,开发语言)