死锁(Dead Lock)

news/2024/11/5 20:38:47 标签: java, jvm, windows, 安全

目录

一. 死锁出现的场景

1. 一个线程, 一个锁对象

2. 两个线程, 两个锁对象

3. N个线程, M个锁对象

二. 造成死锁的必要条件

1. 锁是互斥的

2. 锁是不可被抢占的

3.请求和保持

4. 循环等待

三. 死锁的解决方案

1. 预防死锁

2. 死锁产生后的解决


一. 死锁出现的场景

1. 一个线程, 一个锁对象

当程序中只有一个线程和一个锁对象的时候, 如果这个线程针对这把锁连续加锁两次, 那么就会出现"死锁"的情况.

java"> void func() {
        synchronized(this) { //第一层锁
            synchronized(this) { //第二层锁
                System.out.println("死锁");
            }
        }
    }

如上述代码, 当某一个对象A调用这个方法的时候: 外层的synchronized直接拿到对象A, 顺利执行里面的语句, 执行到内层的synchronized时候, 此时对象A已经被占用, 那么此时内层的synchronized就会阻塞等待, 等待外层的synchronized释放对象A, 而外层的synchronized要释放对象A, 就必须把内层的synchronized执行完. 但是, 内层的synchronized执行完又要外层的synchronized把锁资源释放. 这样就形成了一个死循环, 当代码走到内层的synchronized的时候就会一直阻塞等待. 这就是"死锁"问题.

但是, 这样的逻辑放到java代码中并不会出现死锁问题, 这是因为java底层针对这种情况设计了"可重入锁", "可重入锁"针对两次加锁和多次加锁的情况做了特殊处理. 当一个线程第一次加锁的时候, 会加锁成功, 当后续再再有线程对相同的锁对象加锁时, 系统就会判断当前进行加锁的线程是否是当前锁持有的线程(前一次对这个锁对象加锁的线程), 如果是, 那么这个锁不会进行任何的阻塞操作, 而是"放行", 继续执行里面的代码. 示意图如下:

类似地, 如果嵌套多层: 每次要进行加锁的线程,要获取的锁对象都和前面一样的话,  那么真正加锁的只有最外层, 真正释放锁资源的也只有最外层. 示意图如下:

然而, 在C++ / Python中, 并没有像java这样的机制, 所以1遇到这样的情况就真的会出现"死锁问题". 程序会一直处于"阻塞等待"状态. 

2. 两个线程, 两个锁对象

当有两个线程, 两把锁子时:

第一步: 线程1对锁对象A加锁, 线程2对锁对象B加锁.

第二步: 线程1在不释放A的情况下, 再对B加锁;  线程2在不释放B的情况下, 再对A加锁.

这样操作也会造成"死锁"问题.

具体演示代码如下:

java">public class Demo17 {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                System.out.println("t1 加锁 locker1 完成");

                // 这里的 sleep 是为了确保, t1 和 t2 都先分别拿到 locker1 和 locker2 然后在分别拿对方的锁.
                // 如果没有 sleep 执行顺序就不可控, 可能出现某个线程一口气拿到两把锁, 另一个线程还没执行呢, 无法构造出死锁.
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                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) {
                    throw new RuntimeException(e);
                }

                synchronized (locker2) {
                    System.out.println("t2 加锁 locker2 完成");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

 t1线程拿到locker1后, 想拿locker2, 但是locker2正在被t2线程占有, 需要等待t2线程释放locker2. 而t2线程要能释放locker2, 就必须执行完语句, 必须拿到locker1. 而locker1要想释放t1线程就必须执行完语句, 就必须拿到locker2. 所以现在程序又陷入了一个"死循环"状态. 程序会一直处于"阻塞等待"状态, 形成"死锁".

通过运行结果, 我们看到, t1加锁locker1, t2加锁locker2之后, 程序就形成了"死锁", 进入了"阻塞等待"状态, 不会再往下执行了. 

3. N个线程, M个锁对象

跟上面的第二种情况一样, 这种情景下每个线程需要拿到两个锁, 才能正常的执行完代码. 其他情况都会出现"死锁".

那么我们如何能保证每个线程都能顺利拿到两个锁对象呢? --> 这里就涉及到了操作系统中一个非常经典的问题: 哲学家就餐问题.

现在有五个哲学家围着一个圆桌, 圆桌中心有一份面, 没两个哲学家中间只有一根筷子. 那么我们如何保证每个哲学家都能顺利地吃到面呢? 

我们可以约定, 让哲学家每次拿筷子的时候, 只能拿编号较小的筷子, 那么1, 2号哲学家都会尝试拿1号筷子, 3号哲学家尝试拿2号筷子, 4号哲学家尝试拿3号筷子, 5号哲学家尝试拿4号筷子. 那么这样一来, 5号筷子就空闲了. 所以5号哲学家就会同时拿起4号和5号筷子, 执行完他的任务, 然后放下4号和5号筷子. 此时4号哲学家就会拿起3号和4号筷子, 执行完任务之后放下筷子. 然后3号哲学家拿起2号和3号筷子 ...... 最终1号哲学家拿起1号和5号筷子执行完它的任务, 所有线程顺利执行完毕.

二. 造成死锁的必要条件

1. 锁是互斥的

锁的互斥性是锁的基本特性. 指一个锁资源同一时间只能被一个线程拿到. 一旦一个线程占用了这个锁资源, 其他线程必须等待, 直到这个锁资源资源被释放.

2. 锁是不可被抢占的

锁的不可抢占性也是锁的基本特性. 当某一线程占用了一个锁资源时, 另一线程不能强行抢占这个锁资源. 必须等待这个锁资源被释放.

3.请求和保持

线程t1, 在拿到锁A并且没有释放锁A的前提下, 去拿锁B, 就会产生死锁. (如果线程t1先释放了A, 再去拿B, 是没有任何问题的)

4. 循环等待

线程t1等待线程t2占有的资源, 线程t2等待线程t1占有的资源. 这样就产生了相互等待, 循环依赖的状况. 循环等待的状况一旦产生, 就会一直持续下去, 造成"死锁".

三. 死锁的解决方案

1. 预防死锁

(二)中的四个条件是形成死锁的必要条件, 缺一不可. 不满足其中一个条件就不能形成死锁. 我们也可以根据这个, 破坏四个条件中的任意一个, 就能避免死锁问题的发生.

例如: 我们可以将资源有序分配, 避免因锁资源竞争而产生的等待.  -->  线程必须按照特定的某种顺序请求资源 (我们可以对所有资源进行编号, 令线程只能按照编号的顺序请求资源).

2. 死锁产生后的解决

导致死锁产生之后, 只能通过人工干预来解决. 比如重启服务, 或者kill掉产生死锁的线程.

 好了, 本篇文章就介绍到这里啦, 大家如果有疑问欢迎评论, 如果喜欢小编的文章, 记得点赞收藏~~

 


http://www.niftyadmin.cn/n/5739840.html

相关文章

Java环境下配置环境(jar包)并连接mysql数据库

目录 jar包下载 配置 简单连接数据库 一、注册驱动(jdk6以后会自动注册) 二、连接对应的数据库 以前学习数据库就只是操作数据库,根本不知道该怎么和软件交互,将存储的数据读到软件中去,最近学习了Java连接数据库…

江协科技STM32学习- P36 SPI通信外设

🚀write in front🚀 🔎大家好,我是黄桃罐头,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流 🎁欢迎各位→点赞👍 收藏⭐️ 留言📝​…

LabVIEW适合开发的软件

LabVIEW作为一种图形化编程环境,主要用于测试、测量和控制系统的开发。以下是LabVIEW在不同应用场景中的适用性和优势。 一、测试与测量系统 LabVIEW在测试与测量系统中的应用广泛,是工程测试领域的主流工具之一。利用其强大的数据采集与处理功能&…

5G工业网关的主要功能有哪些?天拓四方

随着5G技术的快速发展和广泛应用,其在工业领域的融合创新日益显著。5G工业网关作为连接工业设备与网络的关键枢纽,正逐步成为推动工业自动化、智能化和数字化的重要力量。 一、5G工业网关的定义 5G工业网关是一种基于5G网络技术的工业通信设备&#xf…

stm32使用串口的轮询模式,实现数据的收发

------内容以b站博主keysking为原型,整理而来,用作个人学习记录。 首先在STM32CubeMX中配置 前期工作省略,只讲重点设置。 这里我配置的是USART2的模式。 会发现,PA2和PA3分别是TX与RX,在连接串口时需要TX对RX&…

IT设备告警预测:运维团队的新导向

在快速变化的IT环境中,运维团队面临着前所未有的挑战。随着业务规模的不断扩大和IT设备的日益复杂,如何确保系统的稳定性和可用性成为了运维工作的重中之重。而在这个过程中,IT设备告警预测作为一项新兴的技术,正逐渐成为运维团队…

介绍目标检测中mAP50和mAP50-95的区别

在目标检测任务中,mAP(mean Average Precision)是一个常用的性能评估指标,用于衡量模型在不同类别和不同IoU(Intersection over Union)阈值下的平均精度。mAP50和mAP50-95是mAP的两个特定版本,它…

gps数据对接G7易流平台

之前伙伴对接G7物流平台获取温度、轨迹数据,写的一塌糊涂,今天来重新对接下。 G7易流 G7物联和易流科技合并后正式发布的品牌,主要面向生产制造与消费物流行业的货主及货运经营者提供软硬一体、全链贯通的SaaS服务。这包括订阅服务&#xff…