1. 概述
在多任务并发环境中,多个线程或进程可能会争夺有限的系统资源。当某个进程请求资源但无法立即获得时,它通常会进入等待状态。然而在某些情况下,这种等待可能永远不会结束,导致三种典型问题:死锁(Deadlock)、活锁(Livelock) 和 资源饥饿(Starvation)。
本文将深入解析这三种并发问题的定义、成因以及常见的应对策略,帮助你在实际开发中识别并规避这些问题。
2. 死锁(Deadlock)
死锁是并发编程中最常见的问题之一,指的是两个或多个线程相互等待对方持有的资源,从而陷入无限等待的状态,无法继续执行。
2.1 什么是死锁?
当多个线程各自持有部分资源,并试图获取其他线程所持有的资源时,如果所有线程都拒绝释放已有资源,就可能进入死锁状态。
下图展示了两个进程相互等待资源的情形:
在这个例子中,进程 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 什么是活锁?
在活锁场景中,线程会不断尝试释放资源以避免死锁,但由于彼此之间的协调失败,导致资源在多个线程之间来回切换,始终无法完成任务。
下图展示了两个进程不断释放资源的场景:
一个现实中的例子是:两个人打电话,都发现对方的电话占线,于是同时挂断并重拨,结果再次同时拨出,导致无限循环。
3.2 死锁 vs 活锁
特征 | 死锁 | 活锁 |
---|---|---|
状态变化 | 无状态变化 | 状态不断变化 |
进度进展 | 完全停滞 | 无实质进展 |
资源状态 | 资源被锁定 | 资源不断释放与重申请 |
可恢复性 | 通常需要外部干预 | 也可能需要外部干预 |
4. 资源饥饿(Starvation)
资源饥饿是指某个线程长期无法获得所需的资源,从而导致任务无法执行。
4.1 什么是资源饥饿?
当线程因为优先级低、资源被其他线程长时间占用等原因,始终无法获得执行机会时,就发生了资源饥饿。
下图展示了进程 2 和进程 3 因为进程 1 长时间占用 CPU 而无法执行的情况:
4.2 资源饥饿的常见原因
资源饥饿可能由以下几种情况导致:
- 死锁或活锁间接引发:资源被锁定或频繁释放,导致某些线程始终无法获取。
- 优先级调度不公平:高优先级线程频繁抢占资源,低优先级线程长期得不到执行。
- 资源被贪婪线程长期占用:某个线程持续申请资源并长时间持有,导致其他线程得不到资源。
4.3 如何避免资源饥饿?
✅ 使用公平调度算法:如轮询(Round Robin)或优先级调度结合“老化(Aging)”机制,定期提升等待时间较长线程的优先级。
// 使用公平锁(ReentrantLock)
ReentrantLock lock = new ReentrantLock(true); // true 表示公平锁
✅ 避免资源长时间占用:合理设计资源使用逻辑,避免单个线程长时间持有资源。
✅ 合理设置线程优先级:确保低优先级线程也有机会获得执行机会。
5. 总结
本文详细介绍了并发编程中常见的三种资源争用问题:
- 死锁:线程相互等待资源,无法继续执行。
- 活锁:线程状态不断变化但无实质进展。
- 资源饥饿:线程因资源分配不公平而长期得不到执行。
这些并发问题在实际开发中非常容易出现,尤其是在多线程和分布式系统中。理解它们的成因和应对策略,有助于你写出更健壮、高效的并发程序。
📌 建议:
- 避免嵌套加锁,尽量减少锁粒度。
- 使用公平锁或优先级调度策略。
- 在设计并发逻辑时,提前考虑资源释放顺序和超时机制。
通过合理设计和测试,可以有效规避这些并发陷阱。