死锁发生的四个条件有哪些

深入理解操作系统与并发编程中的核心难题:死锁。本文详细拆解死锁产生的四个必要条件,提供Java多线程实战案例,并全面解析预防、避免、检测及解除死锁的系统性解决方案。

什么是死锁?

在操作系统和多线程编程中,死锁(Deadlock)是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象。若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁不仅存在于操作系统底层,在现代高并发Java应用、数据库事务处理中也是极其常见且致命的错误。理解死锁发生的四个条件,是解决这一问题的前提。

死锁发生的四个必要条件

要发生死锁,必须同时满足以下四个条件。只要破坏其中任何一个条件,死锁就不会发生。这四个条件分别是:互斥条件、占有并等待条件、非抢占条件和循环等待条件。

🔒

1. 互斥条件 (Mutual Exclusion)

指资源是独占的且排他使用。例如,打印机只能被一个进程使用,当一个进程正在使用某资源时,其他进程必须等待。如果资源允许共享,则不会发生因争夺该资源而导致的死锁。

🤝

2. 占有并等待 (Hold and Wait)

指进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程阻塞,但对自己已获得的资源保持不放。即“手里拿着羊,眼望着羹”。

🚫

3. 非抢占条件 (No Preemption)

指进程已获得的资源在未使用完之前,不能被其他进程强行夺走,只能由进程自己主动释放。操作系统不能强制剥夺进程手中已持有的资源。

🔄

4. 循环等待条件 (Circular Wait)

指在发生死锁时,必然存在一个进程——资源的环形链。即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

这四个条件必须同时成立,死锁才会发生。这是解决死锁问题的理论基石。

Java多线程死锁实战示例

为了更直观地理解这四个条件,我们通过一个经典的Java代码示例来演示死锁是如何产生的。在这个例子中,两个线程Thread-A和Thread-B分别持有锁A和锁B,并试图获取对方的锁。

public class DeadlockDemo {
    public static void main(String[] args) {
        final String lockA = "LockA";
        final String lockB = "LockB";
        Thread threadA = new Thread(() -> {
            synchronized (lockA) {
                System.out.println("Thread A: Holding Lock A ...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread A: Waiting for Lock B ...");
                synchronized (lockB) {
                    System.out.println("Thread A: Holding Lock A & B");
                }
            }
        });
        Thread threadB = new Thread(() -> {
            synchronized (lockB) {
                System.out.println("Thread B: Holding Lock B ...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread B: Waiting for Lock A ...");
                synchronized (lockA) {
                    System.out.println("Thread B: Holding Lock A & B");
                }
            }
        });
        threadA.start();
        threadB.start();
    }
}
                    

代码分析:

  • 互斥条件: synchronized 关键字保证了锁的互斥性。
  • 占有并等待: Thread-A 先获取了 lockA,然后在尝试获取 lockB 时,依然持有 lockA。
  • 非抢占: 一旦获取了锁,JVM不会强制释放,必须等待线程执行完毕或异常。
  • 循环等待: Thread-A 等待 lockB (被 Thread-B 持有),Thread-B 等待 lockA (被 Thread-A 持有),形成闭环。

死锁的预防与避免策略

既然知道了死锁发生的四个条件,我们就可以通过破坏其中至少一个条件来预防死锁,或者使用算法来避免死锁。

破坏四大条件以预防死锁

死锁预防是通过设置限制条件,破坏产生死锁的必要条件之一,从而防止死锁发生。这是一种静态的策略,可能在一定程度上降低系统资源利用率。

  • 破坏互斥条件: 通常不可行,因为很多资源(如打印机)本质上是独占的。但可以使用虚拟设备技术(SPOOLing)将独占设备改造为共享设备。
  • 破坏占有并等待条件:
    • 静态分配(资源预分配): 进程在运行前一次性申请所有需要的资源,只有全部满足时才分配。这破坏了“逐步申请”的条件,但可能导致资源浪费和“饥饿”现象。
    • 释放原有资源: 进程在请求新资源之前,必须释放已持有的所有资源。但这可能导致资源利用效率降低,且可能引起死循环。
  • 破坏非抢占条件: 当进程申请资源不满足时,必须释放已占有的所有资源,待以后重新申请。这实现了资源的强制抢占,但可能导致已进行的工作丢失。
  • 破坏循环等待条件:
    • 资源有序分配法: 将资源编号,进程必须按编号递增顺序请求资源。若需申请编号较大的资源,必须先释放编号较小的资源。这能有效打破环路。

银行家算法 (Banker's Algorithm)

死锁避免是在资源动态分配过程中,防止系统进入不安全状态,从而避免死锁。最常用的算法是银行家算法。

核心思想:

每个进程在运行前必须声明其对资源的最大需求。当进程申请资源时,系统先试探性地分配,然后执行安全性算法检查系统是否仍处于安全状态。

安全状态:

如果系统能按某种进程推进顺序(如 P1, P2, ..., Pn),为每个进程分配其所需的资源,直至满足每个进程对资源的最大需求,使每个进程都可顺利完成,则称系统处于安全状态。若系统处于安全状态,就不会发生死锁。

策略 原理 优点 缺点
死锁预防 破坏必要条件 实现简单,保证不发生死锁 资源利用率低,可能限制并发
死锁避免 银行家算法,避免不安全状态 资源利用率较高 实现复杂,需预先知道最大需求,可能限制进程启动

死锁的检测与解除

在某些情况下,允许死锁发生,但系统需要能够检测出死锁,并在检测到后采取措施解除死锁。这通常用于资源利用率要求高、且死锁发生概率较低的场景。

1. 死锁检测

系统通过资源分配图(Resource Allocation Graph, RAG)或等待图(Wait-For Graph)来检测死锁。

  • 资源分配图简化: 不断消除那些不阻塞的进程所对应的边(即该进程能获得所需资源并运行结束),若最终所有边都能被消除,则无死锁;若图中仍存在环,则存在死锁。
  • 等待图: 只包含进程节点,若进程P等待进程Q的资源,则有一条从P到Q的边。若等待图中存在环,则系统处于死锁状态。

2. 死锁解除

一旦检测到死锁,必须采取措施将其解除。常见方法包括:

资源抢占

从一个或多个进程中抢占足够的资源分配给死锁进程,以打破死锁循环。这需要考虑优先级和回滚机制。

进程终止

强行终止参与死锁的进程。

  • 终止所有死锁进程: 彻底解决,但代价巨大。
  • 逐个终止进程: 每次终止一个进程,直到死锁解除。需选择代价最小的进程(如运行时间最短、优先级最低)。

网友们还关心:死锁的进阶话题

除了基础的四个条件和预防策略,开发者在实际工作中还经常遇到以下相关问题:

如何定位Java死锁?

在Java开发中,可以使用以下工具定位死锁:

  • jstack 命令: 使用 jstack <pid> 查看线程快照,JVM通常会直接报告发现的死锁信息。
  • JConsole / VisualVM: 图形化监控工具,可以直观地看到线程状态和资源锁的持有情况。
  • Arthas: 阿里巴巴开源的Java诊断工具,使用 thread -b 命令可以自动检测并打印死锁线程。

数据库死锁怎么处理?

数据库(如MySQL)中的死锁通常由事务并发引起。解决方法包括:

  • 统一访问顺序: 所有事务以相同的顺序访问资源(如表或行)。
  • 缩短事务: 减少事务持有锁的时间,尽快提交或回滚。
  • 设置超时: 数据库引擎通常有死锁检测机制,超过一定时间未获取锁的事务会被回滚。

关于死锁的常见问答 (FAQ)

死锁和死循环有什么区别?
死锁是指多个线程/进程因互相等待资源而永久阻塞,无法继续执行。死循环是指单个程序执行了无限循环的逻辑,导致CPU占用率100%但业务逻辑无法结束。死锁涉及资源竞争,死循环涉及逻辑错误。
破坏哪个条件最容易实现?
在操作系统层面,破坏循环等待条件(资源有序分配)和破坏占有并等待条件(静态分配)相对容易实现,但可能影响性能。在编程层面,破坏循环等待条件(如始终按固定顺序获取锁)是Java中最常用的预防手段。
Java中ReentrantLock能防止死锁吗?
不能直接防止,但ReentrantLock提供了 tryLock(timeout) 方法,允许线程在等待锁时设置超时。如果超时未获取到锁,线程可以释放已持有的锁并重试,从而在一定程度上打破“非抢占”和“循环等待”条件,降低死锁风险。
✨ 更多精彩
🏷️ 标签:死锁并发控制线程安全互斥锁并发编程数据库事务
研
墨卷研习录
墨卷研习录
CopyRight@educfa.com.cn