• 【JavaEE初阶】 死锁详解


    本文重点:

    • 死锁咋回事

    • 死锁的三个典型情况

    • 死锁的四个必要条件

    • 如何破除死锁

    🎋死锁的概念

    死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。此时就发生了死锁

    🌳死锁的三个典型情况

    🚩一个线程一把锁

    一个线程一把锁,但是都是不可重入锁。该线程争对这个锁连续加锁就会陷入死锁

    在这里插入图片描述

    什么叫不可重入锁呢?为什么不可重入锁会造成死锁呢?

    一个线程没有释放锁, 然后又尝试再次加锁

    • 按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待.

    • 直到第一次的锁被释放, 才能获取到第二个锁.

    • 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作.

    • 这时候就会死锁

    举个例子:

    一个滑稽老铁去上厕所,反锁厕所们后,然后不小心一个闪现出来了,还失忆了,这时候厕所没人,但是处于锁的状态,后面等待的人无法进入
    在这里插入图片描述
    这样的锁称为不可重入锁

    值得注意的是:Java 中的synchronized是可重入锁, 因此没有上面锁死的问题

    🚩两个线程两把锁

    两个线程两把锁,把这两个线程先分别获取一把锁,然后再同时尝试获取对方的锁。
    在这里插入图片描述
    举个例子:

    阿强和阿珍一起去饺子馆吃饺子.
    阿强点了一份汤,阿珍点了一份饺子
    这时候他们都想尝尝对方的东西,这时候就有
    阿强说:你先给我吃饺子,我再给你喝汤
    阿珍说:你先给我和汤,我再给你吃饺子
    如果这俩人彼此之间互不相让, 就构成了死锁.
    饺子和汤相当于是两把锁, 这两个人就是两个线程

    在这里插入图片描述
    这时候阿珍和阿强就会发生僵持,发生死锁

    接下来我我们写个程序来模拟一下

    代码如下:

    public class EatLock {
        public static void main(String[] args) {
            Object soup = new Object();
            Object dumplings = new Object();
    
            Thread thread1 = new Thread(()->{
               synchronized (soup) {
                   //等待是为了让另一个线程有时间给第一层加锁
                   try {
                       Thread.sleep(3000);
    
                   } catch (InterruptedException e) {
                       throw new RuntimeException(e);
                   }
                   synchronized (dumplings) {
                       System.out.println("阿珍喝到了汤,也吃到了饺子");
                   }
               }
            },"阿珍");
            thread1.start();
            Thread thread2 = new Thread(()->{
                synchronized (dumplings) {
                    //等待是为了让另一个线程有时间给第一层加锁
                    try {
                        Thread.sleep(1000);
    
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    synchronized (soup) {
                            System.out.println("阿强吃到了饺子,也喝到了汤");
                    }
                }
            },"阿强");
            thread2.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    代码结果就是堵塞,这里就不展示了

    这里我们通过jconsole命令来进行查看一下各个线程的状态,不会使用的读者可以到博主写的《【JavaEE初阶】线程的概念与创建》里面学习详细的查看方法,这里就不做过多赘述了

    在这里插入图片描述
    在这里插入图片描述
    这里我们可以看到阿强与阿珍都处于等待状态

    🚩n个线程m把锁(哲学家就餐问题)

    问题是这样的:

    • 有个桌子, 围着一圈 哲 学 家, 桌子中间放着一盘意大利面. 每个哲学家两两之间, 放着一根筷子
      在这里插入图片描述
    • 每个 哲 学 家 只做两件事: 思考人生 或者 吃面条. 思考人生的时候就会放下筷子. 吃面条就会拿起左
      右两边的筷子(先拿起左边, 再拿起右边)
      在这里插入图片描述
    • 如果 哲 学 家 发现筷子拿不起来了(被别人占用了), 就会阻塞等待.

    在这里插入图片描述

    • [关键点在这] 假设同一时刻, 五个 哲 ♂ 家 同时拿起左手边的筷子, 然后再尝试拿右手的筷子, 就会发现右手的筷子都被占用了. 由于 哲 ♂ 家 们互不相让, 这个时候就形成了 死锁

    在这里插入图片描述
    死锁是一种严重的 BUG!! 导致一个程序的线程 “卡死”, 无法正常工作

    🎄如何破除死锁

    想要破除死锁,首先的弄懂java死锁的四个必要条件

    java 死锁产生的四个必要条件:

    1. 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用

    2. 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

    3. 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。

    4. 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

    当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

    由于前三个条件是操作系统的特性,所以我们无法改变,我们能做其中最容易破坏的就是 “循环等待”.

    🚩破坏循环等待

    最常用的一种死锁阻止技术就是锁排序. 假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号
    (1, 2, 3…M).

    N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁. 这样就可以避免环路等待.

    比如我们上述阿强与阿珍吃饭的例子

    就是因为两个线程对于加锁的顺序没有约定, 就容易产生环路等待

    解决方法:

    我们预定好先给阿珍喝汤,再给阿强吃饺子,这样就不会产生死锁了,修改代码如下:

    public class EatLock {
        public static void main(String[] args) {
            Object soup = new Object();
            Object dumplings = new Object();
    
            Thread thread1 = new Thread(()->{
               synchronized (soup) {
                   //等待是为了让另一个线程有时间给第一层加锁
                   try {
                       Thread.sleep(3000);
    
                   } catch (InterruptedException e) {
                       throw new RuntimeException(e);
                   }
                   synchronized (dumplings) {
                       System.out.println("阿珍喝到了汤,也吃到了饺子");
                   }
               }
            },"阿珍");
            thread1.start();
            Thread thread2 = new Thread(()->{
                synchronized (soup) {
                    //等待是为了让另一个线程有时间给第一层加锁
                    try {
                        Thread.sleep(1000);
    
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    synchronized (dumplings) {
                            System.out.println("阿强吃到了饺子,也喝到了汤");
                    }
                }
            },"阿强");
            thread2.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    运行结果如下:
    在这里插入图片描述
    哲学家问题也同理,这里就不做过多赘述了

    ⭕总结
    关于《【JavaEE初阶】 死锁》就讲解到这儿,感谢大家的支持,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下!

  • 相关阅读:
    第十章、python字符串操作与with语句及上下文管理器------字符串的操作:字符串的匹配与替换及删除
    10 个你必须要知道的重要JavaScript 数组方法
    Kubernetes:(七)优化大法(江湖失传已久的武林秘籍)
    《游戏引擎架构》 -- 学习2
    多数据源时,时间字段插入数据库前后不一致?
    Centos7安装Clickhouse单节点部署
    力扣每日一题-第27天-561.数组拆分Ⅰ
    Python&C++相互混合调用编程全面实战-29导入pyffmpeg扩展库完成视频的打开
    国民技术 N32G45x 串口踩坑
    小景的Dba之路--如何导出0记录表以及数据泵的使用
  • 原文地址:https://blog.csdn.net/m0_71731682/article/details/133780240