当前位置: 首页 > article >正文

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

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

1. 什么是死锁

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

1.1 一线程一锁

从理论层面来讲,一个线程对应一个锁时,在第一个锁尚未解锁的情况下,是无法添加第二个锁的。然而,在 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 代码来模拟这一过程:

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 代码示例:

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.kler.cn/a/561854.html

相关文章:

  • Shell脚本高级技巧与错误处理
  • WebUI 部署 Ollama 可视化对话界面
  • Directx上传堆和默认堆注意事项
  • WPS中Word表格做好了,忘记写标题了怎么办?
  • 深入理解 Flink 中的 .name() 和 .uid() 方法
  • 网络安全-系统层攻击流程及防御措施
  • web安全——web应用程序技术
  • 智能证件照处理器(深度学习)
  • PHP 连接 Memcached 服务
  • 腾讯云cos 临时密钥 适用于前端直传等临时授权场景
  • 在嵌入式Linux中实现高并发TCP服务器:从select到epoll的演进与实战
  • Spring Cloud源码 - Eureka源码原理分析
  • MFC学习笔记-1
  • 基本网络安全的实现
  • 火语言RPA--Excel设置列宽
  • 分治算法、动态规划、贪心算法、分支限界法和回溯算法的深度对比
  • 十类DeepSeek学术提示词分享
  • 心理咨询小程序的未来发展
  • deepseek部署:ELK + Filebeat + Zookeeper + Kafka
  • WEEX交易所安全教學:如何應對剪切板被劫持駭客攻擊?