大家对死锁都比较熟悉,今天来快速学习一下活锁。
一、活锁的定义与原理
活锁(Livelock) 是并发编程中的问题:线程虽然没有被阻塞(仍在运行),但无法继续执行后续任务,因为彼此不断响应对方的动作,导致系统陷入“无限循环”的无效操作中。
sequenceDiagramparticipant T1 as Thread1participant T2 as Thread2T1->>T1: 获取Lock1T2->>T2: 获取Lock2T1->>T2: 尝试获取Lock2(失败)T2->>T1: 尝试获取Lock1(失败)T1->>T1: 释放Lock1T2->>T2: 释放Lock2T1->>T1: 重新获取Lock1T2->>T2: 重新获取Lock2loop 活锁循环T1->>T2: 尝试获取Lock2(失败)T2->>T1: 尝试获取Lock1(失败)T1->>T1: 释放Lock1T2->>T2: 释放Lock2end
经典场景:
- 两人在走廊相遇,反复避让导致路径持续阻塞。
- 线程A和线程B互相释放资源并重试,导致循环冲突。
二、活锁与死锁的区别
- 死锁:线程互相等待资源,完全停止执行。
- 活锁:线程持续执行,但无法推进任务。
1. 死锁(Deadlock)示意图
graph TDsubgraph "死锁:线程互相等待资源"T1[线程1] -->|持有资源A,请求资源B| R1[资源A]T2[线程2] -->|持有资源B,请求资源A| R2[资源B]T1 -.->|等待资源B| T2T2 -.->|等待资源A| T1end
关键特征:
- 线程互相持有对方需要的资源,且不释放。
- 所有线程被永久阻塞(不再运行)。
- 形成环形等待链(如
T1 → T2 → T1
)。
2. 活锁(Livelock)示意图
graph TDsubgraph "活锁:线程反复释放资源"T1 -->|尝试获取资源B| R2[资源B]T2[线程2] -->|释放资源B| R2[资源B]T2 -->|尝试获取资源A| R1[资源A]T1[线程1] -->|释放资源A| R1[资源A]T1 -.->|失败后重试| T2T2 -.->|失败后重试| T1end
关键特征:
- 线程主动释放资源并重试,但重试策略同步。
- 线程仍在运行,但任务无进展。
- 形成无限循环的无效操作。
对比总结(表格形式)
特征 | 死锁(Deadlock) | 活锁(Livelock) |
---|---|---|
线程状态 | 完全阻塞(停止运行) | 仍在运行(非阻塞) |
资源持有 | 资源被永久占用 | 资源被反复释放和重试获取 |
系统表现 | 无 CPU 占用,任务完全停滞 | 高 CPU 占用,任务无进展 |
解决方式 | 强制终止线程或打破等待链 | 引入随机退避或优先级调度 |
类比场景 | 两人互不让路,僵持原地 | 两人反复避让,始终无法通过 |
通过对比可以看出:
- 死锁是静态的僵局(资源被永久占用)。
- 活锁是动态的僵局(资源被反复释放和重试)。
三、活锁的Java代码示例
以下示例模拟两个线程因资源竞争导致的活锁。示例中,两个线程反复获取和释放锁,但无法同时获得两个锁。
import java.util.concurrent.locks.ReentrantLock;public class LivelockExample {private final ReentrantLock lock1 = new ReentrantLock();private final ReentrantLock lock2 = new ReentrantLock();public void execute() {new Thread(this::process1).start();new Thread(this::process2).start();}private void process1() {while (true) {lock1.lock();System.out.println("⭐Process1 acquired lock1");try {// 关键点:直接尝试获取lock2(无延迟)if (lock2.tryLock()) {System.out.println("⭐Process1 acquired lock2. Working now.");break;} else {System.out.println("⭐Process1 failed to acquire lock2. Retrying...");}} finally {if (lock2.isHeldByCurrentThread()) lock2.unlock();lock1.unlock();}}}private void process2() {while (true) {lock2.lock();System.out.println("👽Process2 acquired lock2");try {// 对称操作:直接尝试获取lock1(无延迟)if (lock1.tryLock()) {System.out.println("👽Process2 acquired lock1. Working now.");break;} else {System.out.println("👽Process2 failed to acquire lock1. Retrying...");}} finally {if (lock1.isHeldByCurrentThread()) lock1.unlock();lock2.unlock();}}}public static void main(String[] args) {new LivelockExample().execute();}
}
示例代码行为分析
步骤 | Process1 动作 | Process2 动作 | 结果 |
---|---|---|---|
1 | 获取 lock1 | 获取 lock2 | 各自持有第一个锁 |
2 | 尝试获取 lock2(失败) | 尝试获取 lock1(失败) | 释放已持有的锁并循环重试 |
3 | 重新获取 lock1 | 重新获取 lock2 | 重复步骤1-2,形成活锁循环 |
输出示例(持续循环):
⭐Process1 acquired lock1
👽Process2 acquired lock2
⭐Process1 failed to acquire lock2. Retrying...
👽Process2 failed to acquire lock1. Retrying...
👽Process2 acquired lock2
👽Process2 failed to acquire lock1. Retrying...
👽Process2 acquired lock2
👽Process2 failed to acquire lock1. Retrying...
👽Process2 acquired lock2
👽Process2 failed to acquire lock1. Retrying...
👽Process2 acquired lock2
👽Process2 failed to acquire lock1. Retrying...
...(无限循环)
四、活锁的检测与解决
检测方法:
- 日志分析:观察日志中线程频繁重试但无进展。
- 性能监控:CPU占用高但任务无完成。
解决方案:
- 随机退避:在重试时引入随机等待(如代码中获取资源时使用
sleep
)。 - 优先级调度:为线程设置不同的重试优先级。
- 超时机制:限制最大重试次数后终止或回退。
五、总结
- 活锁本质:线程因过度“礼貌”导致无效循环。
- 避免关键:通过随机退避、超时或设计资源获取顺序打破对称性。
- 实际应用:在分布式系统、数据库事务中广泛使用退避策略(如指数退避)。
六、活锁(Livelock)高频面试题
1. 活锁与死锁的区别是什么?请从线程状态、资源持有、系统表现三方面说明。
答案:
- 线程状态:
活锁中的线程仍在运行(非阻塞),而死锁中的线程完全阻塞(停止运行)。 - 资源持有:
活锁中线程会主动释放并重试获取资源,死锁中线程永久占用资源不释放。 - 系统表现:
活锁导致高 CPU 占用但任务无进展,死锁导致 CPU 闲置且任务完全停滞。
2. 举一个活锁的实际场景或代码示例,并说明其成因。
答案:
- 场景:
两个线程互相释放资源并重试,例如:// 线程1:先获取锁A,尝试锁B → 失败 → 释放锁A → 重试 // 线程2:先获取锁B,尝试锁A → 失败 → 释放锁B → 重试
- 成因:
线程的重试策略完全对称(如同时释放和重试),导致无限循环。
3. 如何解决活锁问题?至少给出三种方案并说明原理。
答案:
- 随机退避:在重试时加入随机延迟(如
Thread.sleep(random.nextInt(100))
),打破对称性。 - 优先级调度:为线程设置不同的重试优先级(如固定顺序获取资源)。
- 超时机制:限制最大重试次数后终止或回退(如
retryCount > MAX_RETRY
)。
4. 如何检测系统中的活锁?给出具体方法或工具。
答案:
- 日志分析:观察线程日志中频繁的“获取-释放-重试”循环。
- 性能监控:高 CPU 使用率但任务完成率为零。
- 工具检测:
- 使用
jstack
或 VisualVM 查看线程状态(RUNNABLE 但无进展)。 - APM 工具(如 Arthas)监控方法调用频率。
- 使用
5. 在设计并发系统时,如何预防活锁?请结合设计原则说明。
答案:
- 资源有序分配:统一资源获取顺序(如总是按
lock1 → lock2
顺序)。 - 避免对称操作:禁止线程同时释放资源并重试。
- 退避策略集成:在锁机制中内置随机退避(如数据库事务的指数退避算法)。
总结
这些问题覆盖了活锁的核心概念、实际应用和解决方案,是面试中考察并发编程能力的典型题目。回答时需结合代码示例或系统设计经验以体现深度。