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

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

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

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

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

相关推荐
猿周LV1 分钟前
多线程进阶 : 八股文面试题 一 [Java EE 多线程 锁和死锁相关问题]
java·开发语言·java-ee
茂茂在长安12 分钟前
JAVA面试常见题_基础部分_Mysql调优
java·mysql·面试
enyp8023 分钟前
qt QTreeWidget`总结
开发语言·数据库·qt
m0_6845985327 分钟前
心理咨询小程序的未来发展
java·微信小程序·小程序开发·心理咨询小程序·心理测评小程序
PXM的算法星球1 小时前
(java/Spring boot)使用火山引擎官方推荐方法向大模型发送请求
java·spring boot·火山引擎
web_132334214361 小时前
Java实战:Spring Boot application.yml配置文件详解
java·网络·spring boot
神仙别闹1 小时前
基于C#+SQL Server设计与实现的教学管理信息系统
java·数据库·c#
啾啾Fun1 小时前
[java基础-JVM篇]2_垃圾收集器与内存分配策略
java·开发语言·jvm
计算机毕设指导62 小时前
基于Springboot的游戏分享网站【附源码】
java·spring boot·后端·mysql·spring·游戏·maven