什么是死锁?构成死锁的条件如何解决

news/2025/2/26 19:18:06

什么是死锁?构成死锁的条件&如何解决

1. 什么是死锁

在计算机科学中,死锁是一种非常常见且棘手的问题。从线程和锁的角度来看,死锁主要存在三种典型情况:一线程一锁、两线程两锁以及 M 线程 N 锁。接下来,我们将通过具体的实例对这三种情况进行详细剖析。

1.1 一线程一锁

从理论层面来讲,一个线程对应一个锁时,在第一个锁尚未解锁的情况下,是无法添加第二个锁的。然而,在 Java 中存在可重入锁的概念,这就会出现一种看似特殊的情况。以下是具体的情况展示:

通过以下 Java 代码示例,我们可以更直观地理解一线程一锁以及可重入锁的特性:

java">public class Demo1 {
    //一线程一锁->可重入
    public static void main(String[] args) {
        Object lock = new Object();
        Thread t = new Thread(() -> {
            synchronized (lock) {
                synchronized (lock) {
                    System.out.println("可重入锁");
                }
            }
        });
        t.start();
    }
}

在上述代码中,我们创建了一个线程 t,并为其分配了一个锁对象 lock。在线程的执行逻辑中,我们对同一个锁对象 lock 进行了两次 synchronized 操作,这体现了可重入锁的特性,即同一个线程可以多次获取同一个锁,而不会导致死锁。

1.2 两线程两锁

为了更形象地理解两线程两锁导致死锁的情况,我们可以类比一个生活场景:一个人吃饭需要用一双筷子,当两个人只有一双筷子时,就可能出现死锁的情况。假设 A 拿到了 1 只筷子,B 拿到了 1 只筷子,此时两人互不相让,就会陷入僵局,造成死锁。以下是对应的图示:

接下来,我们通过 Java 代码来模拟这一过程:

java">public class Demo2 {
    //两线程两锁

    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();
        Thread t1 = new Thread(() -> {
            //获取lock1
            synchronized (lock1) {
                try {
                    Thread.sleep(10); // 保证t2成功获取lock2
                    //获取lock2
                    synchronized (lock2) {

                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

            System.out.println("t1结束");
        });
        Thread t2 = new Thread(() -> {
            //获取lock2
            synchronized (lock2) {
                try {
                    Thread.sleep(10); // 保证t1成功获取lock1
                    //获取lock2
                    synchronized (lock1) {

                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

            System.out.println("t2结束");

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

    }
}

在上述代码中,我们创建了两个线程 t1 和 t2,以及两个锁对象 lock1 和 lock2。线程 t1 先获取 lock1,然后尝试获取 lock2;线程 t2 先获取 lock2,然后尝试获取 lock1。由于线程之间的竞争和等待,很容易导致死锁的发生。

1.3 M 线程 N 锁

在 M 线程 N 锁的情况中,最典型的例子就是哲学家问题。如下图所示:

现在有 5 个哲学家和 5 根筷子,每个哲学家都需要两根筷子才能吃饭。在下面这种情况下,就会出现死锁:

每个哲学家都只拿到了一只筷子,并且互不相让,这就构成了死锁。

2. 构成死锁的条件

死锁的发生并不是偶然的,它需要满足一定的条件。以下是构成死锁的四个必要条件:

  1. 互斥:当一个线程成功拿到锁之后,其他线程若想要拿到该锁,就必须进入阻塞等待状态。这意味着在同一时刻,一个锁只能被一个线程所拥有。
  2. 不可剥夺:如果线程 1 已经拿到了锁,线程 2 也想要获取该锁,那么线程 2 只能阻塞等待,而无法直接从线程 1 手中剥夺该锁。
  3. 请求和保持:当线程在获取到锁 1 之后,在不释放锁 1 的情况下,又尝试去获取锁 2。例如在上述两线程两锁的问题中,如果线程能够先放下一个筷子(释放一个锁),再去拿另一个筷子(获取另一个锁),就不会构成死锁。
  4. 循环等待:在多个线程的场景中,多把锁的等待过程形成了一个循环。比如在哲学家问题中,A 等待 B 放下筷子,B 等待 C 放下筷子,C 又等待 A 放下筷子,这样就构成了循环等待,从而导致死锁的发生。

3. 解决死锁

既然我们已经了解了死锁的常见情况以及构成死锁的条件,那么接下来我们就来探讨如何解决死锁问题。上述提到的构成死锁的常见情况有三种,其中一线程对一锁的情况,由于 Java 可重入锁的存在,我们在前面已经进行了详细说明,这里就不再赘述。

3.1 二线程对二锁

对于二线程对二锁导致死锁的问题,解决方法其实并不复杂。我们只需要将并行执行的方式改为顺序执行即可。具体的执行顺序为:t1 得到 lock1 ——> t1 释放 lock1 ——> t1 得到 lock2 ——> t1 释放 lock2 ——> t1 线程结束,t2 线程同理。以下是正确的 Java 代码示例:

java">public class Demo2 {
    //两线程两锁

    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();
        Thread t1 = new Thread(() -> {
            //获取lock1
            synchronized (lock1) {
                try {
                    Thread.sleep(10); // 保证t2成功获取lock2
                    //获取lock2

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }
            synchronized (lock2) {

            }
            System.out.println("t1结束");
        });
        Thread t2 = new Thread(() -> {
            //获取lock2
            synchronized (lock2) {
                try {
                    Thread.sleep(10); // 保证t1成功获取lock1
                    //获取lock2

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }
            synchronized (lock1) {

            }

            System.out.println("t2结束");

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

    }
}

通过这种顺序执行的方式,我们可以有效地避免两个线程之间因为竞争锁而导致的死锁问题。

3.2 M 线程 N 锁

解决 M 线程 N 锁导致的死锁问题,关键在于打破构成死锁条件中的第四条——循环等待。这里我们仍然以哲学家问题为例,一种有效的解决方法是约定好用餐顺序,从而实现串行化。以下是对应的图示:

通过约定用餐顺序,我们可以确保每个哲学家都能够按照一定的规则获取和释放筷子,从而避免了循环等待的发生,进而有效地解决了死锁问题。

综上所述,死锁是一个在多线程编程中需要特别关注的问题。通过深入理解死锁的概念、构成死锁的条件以及相应的解决方法,我们可以在实际编程中有效地避免死锁的发生,提高程序的稳定性和可靠性。


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

相关文章

微信小程序源码逆向 MacOS

前言 日常工作中经常会遇到对小程序的渗透测试,微信小程序的源码是保存在用户客户端本地,在渗透的过程中我们需要提取小程序的源码进行问题分析,本篇介绍如何在苹果电脑 MacOS 系统上提取微信小程序的源码。 0x01 微信小程序提取 在苹果电…

HTML解析 → DOM树 CSS解析 → CSSOM → 合并 → 渲染树 → 布局 → 绘制 → 合成 → 屏幕显示

一、关键渲染流程 解析 HTML → 生成 DOM 树 浏览器逐行解析 HTML&#xff0c;构建**DOM&#xff08;文档对象模型&#xff09;**树状结构 遇到 <link> 或 <style> 标签时会暂停 HTML 解析&#xff0c;开始加载 CSS 解析 CSS → 生成 CSSOM 将 CSS 规则解析为**…

Pycharm-Version: 2024.3.3导入conda环境

打开一个新项目&#xff0c;点击File->Settings 找到Project->python interpreter 新增环境&#xff0c;点击add interpreter->add local interpreter 点击select existing->conda&#xff0c;选择地址为&#xff1a;anoconda/library/bin/conda.bat&#xff0c…

Fisher散度:从信息几何到机器学习的隐藏利器

Fisher散度&#xff1a;从信息几何到机器学习的隐藏利器 在机器学习和统计学中&#xff0c;比较两个概率分布的差异是常见任务&#xff0c;比如评估真实分布与模型预测分布的差距。KL散度&#xff08;Kullback-Leibler Divergence&#xff09;可能是大家熟悉的选择&#xff0c…

C++ | 高级教程 | 信号处理

&#x1f47b; 概念 信号 —— 操作系统传给进程的中断&#xff0c;会提早终止程序有些信号不能被程序捕获&#xff0c;有些则可以被捕获&#xff0c;并基于信号采取适当的动作 信号描述SIGABRT程序的异常终止&#xff0c;如调用 abortSIGFPE错误的算术运算&#xff0c;比如除…

文件上传漏洞学习笔记

一、漏洞概述 定义 文件上传漏洞指未对用户上传的文件进行充分安全校验&#xff0c;导致攻击者可上传恶意文件&#xff08;如Webshell、木马&#xff09;&#xff0c;进而控制服务器或执行任意代码。 危害等级 ⚠️ 高危漏洞&#xff08;通常CVSS评分7.0&#xff09;&#xff…

PDF转HTML 超级好用 免费在线转换PDF 完美转换格式

PDF转HTML 超级好用 免费在线转换PDF 完美转换格式&#xff0c;PDF已成为一种广泛使用的文件格式&#xff0c;用于保存和分享文档。然而&#xff0c;PDF文件在某些场景下可能不够灵活&#xff0c;特别是在需要在网页上直接展示其内容时。为了满足这一需求&#xff0c;小白工具推…

自定义提交按钮触发avue-form绑定的submit事件

场景 使用avue-form时&#xff0c;提交按钮会绑定至form区域下方&#xff0c;如果想自定义按钮位置&#xff0c;需要通过dialog的footer位置进行编写&#xff0c;例如&#xff1a; <avue-form ref"form" v-model"dataInfo" :option"dataInfoOpti…