1. 概述

在多任务并发环境中,多个线程或进程可能会争夺有限的系统资源。当某个进程请求资源但无法立即获得时,它通常会进入等待状态。然而在某些情况下,这种等待可能永远不会结束,导致三种典型问题:死锁(Deadlock)活锁(Livelock)资源饥饿(Starvation)

本文将深入解析这三种并发问题的定义、成因以及常见的应对策略,帮助你在实际开发中识别并规避这些问题。


2. 死锁(Deadlock)

死锁是并发编程中最常见的问题之一,指的是两个或多个线程相互等待对方持有的资源,从而陷入无限等待的状态,无法继续执行。

2.1 什么是死锁?

当多个线程各自持有部分资源,并试图获取其他线程所持有的资源时,如果所有线程都拒绝释放已有资源,就可能进入死锁状态。

下图展示了两个进程相互等待资源的情形:

Deadlock

在这个例子中,进程 1 持有资源 A 并请求资源 B,而进程 2 持有资源 B 并请求资源 A。两者都无法继续执行,形成死锁。

2.2 死锁发生的必要条件

要构成死锁,必须同时满足以下四个条件:

互斥(Mutual Exclusion):至少有一个资源不能共享,只能由一个线程独占使用。
持有并等待(Hold and Wait):线程在等待其他资源时,并不释放自己已持有的资源。
不可抢占(No Preemption):资源只能由持有它的线程主动释放,不能被强制剥夺。
循环等待(Circular Wait):存在一个线程链,每个线程都在等待下一个线程所持有的资源。

2.3 如何避免死锁?

要避免死锁,只需破坏上述四个条件中的任意一个即可:

破坏互斥:有些资源(如只读文件)可以允许多个线程共享,但对于互斥锁(如 Mutex)则无法避免。
破坏“持有并等待”:线程在申请资源前应一次性申请所有所需资源,否则不分配任何资源。
破坏“不可抢占”:允许系统强制回收资源,但可能造成数据不一致。
破坏“循环等待”:为资源分配一个全局顺序编号,线程只能按编号顺序申请资源。

例如,资源编号策略如下:

// 资源编号
Resource A = new Resource(1);
Resource B = new Resource(2);

// 线程1先申请编号较小的资源
synchronized (A) {
    synchronized (B) {
        // do something
    }
}

// 线程2也按编号顺序申请资源
synchronized (A) {
    synchronized (B) {
        // do something
    }
}

3. 活锁(Livelock)

活锁是死锁的变种,虽然线程状态在不断变化,但仍然无法取得任何实质进展。

3.1 什么是活锁?

在活锁场景中,线程会不断尝试释放资源以避免死锁,但由于彼此之间的协调失败,导致资源在多个线程之间来回切换,始终无法完成任务。

下图展示了两个进程不断释放资源的场景:

Livelock

一个现实中的例子是:两个人打电话,都发现对方的电话占线,于是同时挂断并重拨,结果再次同时拨出,导致无限循环。

3.2 死锁 vs 活锁

特征 死锁 活锁
状态变化 无状态变化 状态不断变化
进度进展 完全停滞 无实质进展
资源状态 资源被锁定 资源不断释放与重申请
可恢复性 通常需要外部干预 也可能需要外部干预

4. 资源饥饿(Starvation)

资源饥饿是指某个线程长期无法获得所需的资源,从而导致任务无法执行。

4.1 什么是资源饥饿?

当线程因为优先级低、资源被其他线程长时间占用等原因,始终无法获得执行机会时,就发生了资源饥饿。

下图展示了进程 2 和进程 3 因为进程 1 长时间占用 CPU 而无法执行的情况:

Starvation

4.2 资源饥饿的常见原因

资源饥饿可能由以下几种情况导致:

  • 死锁或活锁间接引发:资源被锁定或频繁释放,导致某些线程始终无法获取。
  • 优先级调度不公平:高优先级线程频繁抢占资源,低优先级线程长期得不到执行。
  • 资源被贪婪线程长期占用:某个线程持续申请资源并长时间持有,导致其他线程得不到资源。

4.3 如何避免资源饥饿?

使用公平调度算法:如轮询(Round Robin)或优先级调度结合“老化(Aging)”机制,定期提升等待时间较长线程的优先级。

// 使用公平锁(ReentrantLock)
ReentrantLock lock = new ReentrantLock(true); // true 表示公平锁

避免资源长时间占用:合理设计资源使用逻辑,避免单个线程长时间持有资源。

合理设置线程优先级:确保低优先级线程也有机会获得执行机会。


5. 总结

本文详细介绍了并发编程中常见的三种资源争用问题:

  • 死锁:线程相互等待资源,无法继续执行。
  • 活锁:线程状态不断变化但无实质进展。
  • 资源饥饿:线程因资源分配不公平而长期得不到执行。

这些并发问题在实际开发中非常容易出现,尤其是在多线程和分布式系统中。理解它们的成因和应对策略,有助于你写出更健壮、高效的并发程序。

📌 建议

  • 避免嵌套加锁,尽量减少锁粒度。
  • 使用公平锁或优先级调度策略。
  • 在设计并发逻辑时,提前考虑资源释放顺序和超时机制。

通过合理设计和测试,可以有效规避这些并发陷阱。


原始标题:Deadlock, Livelock and Starvation