【JavaSE专栏79】线程死锁,多个线程被阻塞,等待彼此持有的资源

简介: 【JavaSE专栏79】线程死锁,多个线程被阻塞,等待彼此持有的资源

本文讲解了 Java 中线程死锁的语法和应用场景,并给出了样例代码。线程死锁是指在多线程编程中,两个或多个线程被永久地阻塞,等待彼此持有的资源,而无法继续执行下去。


一、什么是线程死锁

线程死锁是指在多线程编程中,两个或多个线程被永久地阻塞,等待彼此持有的资源,而无法继续执行下去,这种情况下,被阻塞的线程将无法释放它所持有的资源,导致所有的线程都无法继续工作。

线程死锁通常发生在多个线程同时试图获取共享资源的情况下,而每个线程都在等待其他线程释放它所需要的资源。这种情况下,没有任何一个线程能够继续执行下去,形成了死锁。

线程死锁的产生通常需要满足以下 4 44 个条件,称为死锁的必要条件,请同学们认真学习。

  1. 互斥条件:至少有一个资源被且只能被一个线程持有。
  2. 请求与保持条件:一个线程在持有资源的同时又请求其他线程持有的资源。
  3. 不可剥夺条件:已经分配给一个线程的资源不能被其他线程强制性地抢占。
  4. 循环等待条件:存在一个线程的资源请求序列,使得每个线程都在等待下一个线程所持有的资源。

要避免线程死锁,可以采取一些预防措施,如避免循环等待、确保资源的合理分配和释放、使用加锁机制来保证互斥等,此外还可以使用死锁检测和死锁恢复机制来解决线程死锁问题。


二、线程死锁的产生原因

线程死锁的产生原因通常是由于多个线程之间竞争共享资源时出现的一系列问题,以下是几种常见的线程死锁的产生原因,请同学们认真学习。

  1. 竞争资源:多个线程同时竞争有限的资源,当每个线程都持有部分资源并且等待其他线程释放它需要的资源时,就会发生死锁。
  2. 循环等待:多个线程之间形成循环依赖,每个线程都在等待其他线程所持有的资源,导致循环等待的状态。
  3. 独占资源无法释放:一个线程持有某个资源并且不释放,而其他线程需要该资源时无法继续执行,最终导致死锁。
  4. 加锁顺序不一致:多个线程按照不同的顺序请求资源和释放资源,导致出现资源竞争的情况,可能引发死锁。
  5. 阻塞、等待或者睡眠:线程在等待某个操作完成或者等待其他线程的通知时,如果等待的时间过长,可能导致其他线程无法继续执行,最终导致死锁。

为了避免线程死锁的发生,需要合理设计和管理线程的竞争和资源的分配,确保资源的合理使用和释放,避免循环依赖和死锁的条件。同时,可以使用死锁检测和死锁恢复机制来解决死锁问题。


三、线程死锁的模拟

下面是一个使用 Java 语言模拟线程死锁的样例代码,请同学们复制到本地执行。

public class ThreadDeadlock {
    public static void main(String[] args) {
        final Object resource1 = new Object();
        final Object resource2 = new Object();
        // 线程1
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1: Holding resource 1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resource2) {
                    System.out.println("Thread 1: Holding resource 1 & 2");
                }
            }
        });
        // 线程2
        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread 2: Holding resource 2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resource1) {
                    System.out.println("Thread 2: Holding resource 2 & 1");
                }
            }
        });
        // 启动线程
        thread1.start();
        thread2.start();
    }
}

在上述代码中,有两个线程 thread1thread2,它们都试图获取两个资源 resource1resource2。然而,线程 1 11 首先获取了 resource1,然后试图获取 resource2;而线程 2 22 首先获取了 resource2,然后试图获取 resource1,由于两个线程都在等待对方释放所需要的资源,因此会造成死锁的情况。


四、线程死锁的应用场景

线程死锁是一个不可避免的问题,它可能发生在任何多线程应用程序中的特定条件下。以下是一些常见的线程死锁的应用场景,请同学们认真学习。

  1. 资源竞争:当多个线程同时竞争有限的资源时,例如数据库连接池、文件系统、网络资源等,如果处理不当,可能会导致线程死锁。
  2. 锁的嵌套使用:当多个线程按不同的顺序请求锁时,如果锁的嵌套使用不当,可能会导致发生死锁。
  3. 线程间的相互依赖:当多个线程之间存在依赖关系,需要等待其他线程释放资源时,如果依赖关系不正确或者线程等待时间过长,可能会导致死锁。
  4. 死锁的传播:当一个线程发生死锁,它可能会导致其他线程也被阻塞,从而形成死锁链。
  5. 死锁的循环等待:当多个线程发生循环等待的情况,每个线程都在等待其他线程所持有的资源时,可能会导致发生死锁。

虽然线程死锁是一个常见的多线程编程问题,但并非所有的多线程应用程序都会发生死锁,正确设计和管理线程之间的资源竞争、避免循环依赖、合理分配和释放资源等措施可以降低死锁的发生概率。


五、线程死锁面试题

问题

请说明什么是线程死锁?并举一个实例来解释

回答

线程死锁是指两个或多个线程彼此持有对方所需的资源,而无法继续执行的状态。在这种情况下,每个线程都在等待其他线程释放它所需要的资源,导致所有线程无法继续执行下去,形成了死锁。

实例

假设有两个线程 A AAB BB,每个线程都需要获取对方持有的资源才能继续执行。线程 A AA 持有资源 X XX,但需要资源 Y YY 才能继续执行;线程 B BB 持有资源 Y YY,但需要资源 X XX 才能继续执行。因此,线程 A AA 等待线程 B BB 释放资源 Y YY,而线程 B BB 等待线程 A AA 释放资源 X XX,由于两个线程都无法继续执行,它们进入了死锁状态。

代码示例

public class ThreadDeadlock {
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();
    public static void main(String[] args) {
        // 线程1
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1: Holding resource 1.");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resource2) {
                    System.out.println("Thread 1: Holding resource 1 and 2.");
                }
            }
        });
        // 线程2
        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread 2: Holding resource 2.");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resource1) {
                    System.out.println("Thread 2: Holding resource 1 and 2.");
                }
            }
        });
        // 启动线程
        thread1.start();
        thread2.start();
    }
}

在上述代码中,线程 1 11 持有资源 1 11 并等待资源 2 22,而线程 2 22 持有资源 2 22 并等待资源 1 11。因此,当两个线程同时执行时,它们会相互等待对方释放所需的资源,导致线程死锁。


六、总结

本文讲解了 Java 中线程死锁的语法和应用场景,并给出了样例代码,在下一篇博客中,将讲解 Java 的多线程通信问题。


相关文章
|
1月前
|
Java 数据库连接 数据库
不同业务使用同一个线程池发生死锁的技术探讨
【10月更文挑战第6天】在并发编程中,线程池是一种常用的优化手段,用于管理和复用线程资源,减少线程的创建和销毁开销。然而,当多个不同业务场景共用同一个线程池时,可能会引发一系列并发问题,其中死锁就是最为严重的一种。本文将深入探讨不同业务使用同一个线程池发生死锁的原因、影响及解决方案,旨在帮助开发者避免此类陷阱,提升系统的稳定性和可靠性。
46 5
|
1月前
|
安全 Java 程序员
【多线程-从零开始-肆】线程安全、加锁和死锁
【多线程-从零开始-肆】线程安全、加锁和死锁
43 0
|
3月前
|
安全 算法 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(下)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
75 6
|
3月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(中)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
83 5
|
3月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(上)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
83 3
【多线程面试题十二】、阻塞线程的方式有哪些?
线程阻塞的方式包括调用sleep()方法、阻塞式IO操作、等待同步监视器的获取、等待通知(notify),以及慎用的suspend()方法。
|
4月前
|
Java C# Python
线程等待(Thread Sleep)
线程等待(Thread Sleep)
|
4月前
|
测试技术
三种等待方式(‌线程等待、‌隐式等待、‌显式等待)
三种等待方式(‌线程等待、‌隐式等待、‌显式等待)
198 4
|
3月前
|
Java
Java多线程-死锁的出现和解决
死锁是指多线程程序中,两个或以上的线程在运行时因争夺资源而造成的一种僵局。每个线程都在等待其中一个线程释放资源,但由于所有线程都被阻塞,故无法继续执行,导致程序停滞。例如,两个线程各持有一把钥匙(资源),却都需要对方的钥匙才能继续,结果双方都无法前进。这种情况常因不当使用`synchronized`关键字引起,该关键字用于同步线程对特定对象的访问,确保同一时刻只有一个线程可执行特定代码块。要避免死锁,需确保不同时满足互斥、不剥夺、请求保持及循环等待四个条件。
|
3月前
|
Java 测试技术 PHP
父子任务使用不当线程池死锁怎么解决?
在Java多线程编程中,线程池有助于提升性能与资源利用效率,但若父子任务共用同一池,则可能诱发死锁。本文通过一个具体案例剖析此问题:在一个固定大小为2的线程池中,父任务直接调用`outerTask`,而`outerTask`再次使用同一线程池异步调用`innerTask`。理论上,任务应迅速完成,但实际上却超时未完成。经由`jstack`输出的线程调用栈分析发现,线程陷入等待状态,形成“死锁”。原因是子任务需待父任务完成,而父任务则需等待子任务执行完毕以释放线程,从而相互阻塞。此问题在测试环境中不易显现,常在生产环境下高并发时爆发,重启或扩容仅能暂时缓解。