网站首页 > 文章精选 正文
这是一个经典的并发编程面试题。理解死锁、活锁和饥饿的区别对于设计和诊断健壮的并发系统至关重要。下面详细解释它们的区别:
核心概念区别
特征 | 死锁 (Deadlock) | 活锁 (Livelock) | 饥饿 (Starvation) |
定义 | 两个或多个线程/进程永久阻塞,每个都持有对方需要的资源并等待对方释放自己需要的资源。 | 线程/进程持续运行(不阻塞),但不断改变状态以避免死锁或冲突,导致无法取得实质性进展。 | 一个或多个线程/进程长期或永久无法获取所需资源(通常是CPU时间片或锁),导致无法执行或进展极其缓慢。 |
状态 | 阻塞状态 (Blocked/Waiting)。线程停止执行,等待永远不会到来的资源。 | 运行状态 (Runnable/Running)。线程仍在消耗CPU,但没有实际工作完成。 | 可运行状态 (Runnable),但很少或从未被调度执行;或者在等待资源,但资源总是被优先分配给其他线程。 |
资源持有 | 持有至少一个资源,并等待获取另一个被其他线程持有的资源。 | 不持有任何阻塞性资源(或短暂持有后立即释放),或者主动让出资源以避免冲突。 | 不持有所需的资源,并尝试获取但总是失败。 |
进展 | 完全无进展。系统完全卡住。 | 表面上有进展(线程在运行),但实质无进展(任务未完成)。 | 部分线程无进展或进展极慢,而其他线程正常推进。 |
原因 | 循环等待 + 互斥 + 不可抢占 + 持有并等待(四个必要条件同时满足)。 | 过于“礼貌”或“退让”的冲突解决策略,导致双方不断重复相同的退让动作。 | 资源分配策略不公平(如低优先级线程、锁竞争策略不合理、资源不足)。 |
解决方案 | 破坏必要条件之一(如引入超时、按顺序获取锁、死锁检测与恢复)。 | 引入随机性或退让策略(如随机等待时间、退让次数限制)。 | 实现公平性机制(公平锁、优先级调整、轮转调度、确保资源充足)。 |
是否可恢复 | 通常需要外部干预(超时机制、人工重启)才能恢复。 | 可能自行恢复(如果随机性或策略导致一方成功),但概率较低,通常也需要干预。 | 可能自行恢复(如果高优先级线程完成),但也可能永久持续,需调整策略。 |
深入解析与对比
1. 死锁 (Deadlock) vs. 活锁 (Livelock)
- 核心区别:状态与进展
- 死锁: 线程被阻塞,不做任何事,完全僵持。就像两辆车在一条窄路上迎面相遇,谁也不肯倒车让路,都停在那里不动了。
- 活锁: 线程没有阻塞,在“忙”,但这种“忙”是在做无效的协调工作(反复尝试、失败、重试相同的策略)。就像走廊里两个人迎面走来,都想给对方让路,结果你向左我也向左,你向右我也向右,不断重复避让却始终面对面挡着路,无法通过。
- 比喻:
- 死锁: 两个骑士在独木桥上相遇,互不相让,都举着盾牌和剑僵持着,谁也无法前进或后退(永久阻塞)。
- 活锁: 两个骑士在独木桥上相遇,都想让对方先过。骑士A说“您先请”并后退一步,同时骑士B也说“您先请”也后退一步,结果两人同时后退又同时回到桥中间。他们不断重复这个“礼貌退让”的动作,虽然都在动,但谁也没过桥(无实质进展)。
2. 死锁 (Deadlock) vs. 饥饿 (Starvation)
- 核心区别:资源持有与原因
- 死锁: 涉及多个线程,每个线程都持有部分资源,并等待其他线程持有的资源。是相互阻塞的循环。所有相关线程都停滞。
- 饥饿: 通常涉及一个或多个线程,这些线程无法获得所需资源(通常是CPU时间或锁),而其他线程(通常优先级更高或更“幸运”)能持续获得资源并执行。饥饿的线程没有持有阻塞性资源,只是在等待。系统整体可能仍在推进(因为有线程在执行),但部分线程被“饿死”。
- 比喻:
- 死锁: 四个哲学家围坐一桌,每人左边有一把叉子。要吃饭需要同时拿到左边和右边的叉子。如果每个人都拿起自己左边的叉子,然后等待右边的叉子,那么所有人都会永远等待下去(持有资源并等待他人释放)。
- 饥饿: 一个水龙头前有很多人排队打水。总有几个强壮的人(高优先级)每次都能挤到前面打到水。而一个瘦弱的人(低优先级)每次都被挤到最后面,永远轮不到他打水(无法获得资源)。
3. 活锁 (Livelock) vs. 饥饿 (Starvation)
- 核心区别:线程状态与原因
- 活锁: 线程在运行(消耗CPU),但行为是无效的循环尝试。通常由主动的冲突解决策略导致(过度退让)。相关线程都在“忙活”但没结果。
- 饥饿: 线程大部分时间处于可运行状态(等待调度)或等待资源状态,但很少或从未被调度执行或获得资源。由被动的、不公平的资源分配策略导致(调度算法、锁策略)。饥饿线程通常很“安静”(没机会运行)。
Java 代码示例 (简化版)
死锁示例
java
public class DeadlockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread1: Holding lockA...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread1: Waiting for lockB...");
synchronized (lockB) {
System.out.println("Thread1: Acquired lockB!");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lockB) { // 与 thread1 获取锁的顺序相反
System.out.println("Thread2: Holding lockB...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread2: Waiting for lockA...");
synchronized (lockA) {
System.out.println("Thread2: Acquired lockA!");
}
}
});
thread1.start();
thread2.start();
}
}
- 结果: Thread1 持有 lockA 等待 lockB,Thread2 持有 lockB 等待 lockA。两者永久阻塞。输出会卡在 Thread1: Waiting for lockB... 和 Thread2: Waiting for lockA...。
活锁示例 (过度退让)
java
public class LivelockDemo {
static class Spoon {
private Diner owner;
public Spoon(Diner d) { owner = d; }
public synchronized void use() { System.out.println(owner.name + " is eating!"); }
}
static class Diner {
private String name;
private boolean isHungry;
public Diner(String n) { name = n; isHungry = true; }
public void eatWith(Spoon spoon, Diner spouse) {
while (isHungry) {
// 如果勺子不是自己的,就等一下再试 (过度礼貌)
if (spoon.owner != this) {
try { Thread.sleep(100); } catch (InterruptedException e) { continue; }
continue;
}
// 如果配偶也饿,先让给配偶 (过度退让)
if (spouse.isHungry) {
System.out.println(name + ": You eat first, dear " + spouse.name);
spoon.owner = spouse; // 把勺子让出去
continue;
}
// 自己吃
spoon.use();
isHungry = false;
System.out.println(name + ": I'm done eating!");
spoon.owner = spouse; // 吃完后把勺子给配偶
}
}
}
public static void main(String[] args) {
final Diner husband = new Diner("Bob");
final Diner wife = new Diner("Alice");
final Spoon spoon = new Spoon(husband); // 勺子初始属于丈夫
Thread t1 = new Thread(() -> husband.eatWith(spoon, wife));
Thread t2 = new Thread(() -> wife.eatWith(spoon, husband));
t1.start();
t2.start();
}
}
- 结果: Bob 和 Alice 都非常礼貌且饥饿。Bob 看到勺子是他的,但发现 Alice 也饿,就说“Alice 你先吃”并把勺子让给 Alice。同时 Alice 看到勺子现在是她的了,但发现 Bob 也饿,就说“Bob 你先吃”并把勺子让回给 Bob。这个过程无限循环,两人都在运行 (while (isHungry)),都在让勺子 (spoon.owner = spouse),但谁也没吃到 (spoon.use() 从未执行)。控制台会不断输出 Bob: You eat first, dear Alice 和 Alice: You eat first, dear Bob。
饥饿示例 (不公平锁竞争)
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class StarvationDemo {
private static final Lock lock = new ReentrantLock(); // 默认非公平锁,可能加剧饥饿
public static void main(String[] args) {
// 高优先级线程 (更容易抢到锁)
Thread highPriorityThread = new Thread(new Worker("High-Prio"));
highPriorityThread.setPriority(Thread.MAX_PRIORITY);
highPriorityThread.start();
// 多个低优先级线程 (很难抢到锁)
for (int i = 0; i < 5; i++) {
Thread lowPriorityThread = new Thread(new Worker("Low-Prio-" + i));
lowPriorityThread.setPriority(Thread.MIN_PRIORITY);
lowPriorityThread.start();
}
}
static class Worker implements Runnable {
private String name;
public Worker(String name) { this.name = name; }
@Override
public void run() {
while (true) {
lock.lock(); // 尝试获取锁
try {
System.out.println(name + " acquired the lock and is working...");
// 模拟工作耗时
try { Thread.sleep(100); } catch (InterruptedException e) {}
} finally {
lock.unlock();
}
// 短暂休眠,模拟其他工作
try { Thread.sleep(10); } catch (InterruptedException e) {}
}
}
}
}
- 结果 (可能): High-Prio 线程由于其高优先级和 ReentrantLock 默认的非公平特性,更频繁、更容易地获取到锁并执行工作。而 Low-Prio-0 到 Low-Prio-4 这些低优先级线程,很难甚至几乎无法获取到锁(输出中很少或几乎看不到它们获取锁的消息)。它们在 lock.lock() 处等待,或者在 Runnable 状态等待调度器分配CPU时间片(但调度器倾向于选择高优先级线程),从而长期无法执行实际工作,处于饥饿状态。
总结要点 (面试回答)
- 死锁 vs 活锁:
- 死锁: 线程阻塞不动,持有资源等待另一个资源,无任何进展。是僵局。
- 活锁: 线程持续运行,不持有关键资源(或主动释放),不断尝试避让冲突,但无实质进展。是无效忙碌。
- 死锁 vs 饥饿:
- 死锁: 多个线程相互阻塞,都持有部分资源并等待对方资源,所有相关线程都停滞。
- 饥饿: 一个或多个线程长期无法获取资源(CPU时间或锁),其他线程能正常执行。饥饿线程在等待状态,未持有阻塞性资源。
- 活锁 vs 饥饿:
- 活锁: 线程在运行,因过度协调/避让策略导致无效循环。
- 饥饿: 线程很少或从未被调度运行,或无法获得锁,因资源分配不公导致。
关键记忆点:
- 死锁 = 堵死不动 (Blocked, Hold & Wait)
- 活锁 = 瞎忙活 (Running, No Progress)
- 饥饿 = 轮不到 (Runnable but Never Scheduled, Can't Acquire Resource)
理解这些区别及其成因,有助于在设计和调试并发程序时识别问题并选择合适的解决方案(如锁顺序、超时、随机退避、公平锁、优先级调整等)。
猜你喜欢
- 2025-07-10 Java基础——Java多线程(Lock接口详解)
- 2025-07-10 高并发环境下诡异的加锁问题(你加的锁未必安全)
- 2025-07-10 你真正了解synchronized关键字吗?
- 2025-07-10 Java 并发之 ReentrantReadWriteLock 深入分析
- 2025-07-10 ReentrantLock源码解析:ReentrantLock 的实现原理与 AQS 机制
- 2025-07-10 「Java多线程」内置锁(Synchronized)的前世今生
- 2025-07-10 面试:如何保证接口的幂等性?常见的实现方案有哪些?
- 2025-07-10 聊聊并发编程: Lock(并发编程的三大特性)
- 2025-07-10 Java并发之旅:Lock, Condition & ReadWriteLock 的魔法
- 2025-07-10 对volatile,synchronized,AQS的加锁解锁原理的一些理解
- 最近发表
-
- Vue3+Django4全新技术实战全栈项目|高清完结
- 工厂模式+策略模式消除 if else 实战
- 每天一个 Python 库:httpx异步请求,让接口测试飞起来
- 如何高效实现API接口的自动化测试?
- 前端工程化:从“手忙脚乱”到“从容协作”的进化记
- 使用C#创建服务端Web API(c#开发web服务器)
- SpringBoot之旅第四篇-web开发(springboot做web项目)
- 一文读懂SpringMVC(一文读懂新型政策性金融工具)
- Rust Web编程:第十二章 在 Rocket 中重新创建我们的应用程序
- Apache Druid 数据摄取——本地数据和kafka流式数据 一篇文章看懂
- 标签列表
-
- newcoder (56)
- 字符串的长度是指 (45)
- drawcontours()参数说明 (60)
- unsignedshortint (59)
- postman并发请求 (47)
- python列表删除 (50)
- 左程云什么水平 (56)
- 计算机网络的拓扑结构是指() (45)
- 编程题 (64)
- postgresql默认端口 (66)
- 数据库的概念模型独立于 (48)
- 产生系统死锁的原因可能是由于 (51)
- 数据库中只存放视图的 (62)
- 在vi中退出不保存的命令是 (53)
- 哪个命令可以将普通用户转换成超级用户 (49)
- noscript标签的作用 (48)
- 联合利华网申 (49)
- swagger和postman (46)
- 结构化程序设计主要强调 (53)
- 172.1 (57)
- apipostwebsocket (47)
- 唯品会后台 (61)
- 简历助手 (56)
- offshow (61)
- mysql数据库面试题 (57)