三分钟掌握活锁(Livelock)
大家对死锁都比较熟悉,今天来快速学习一下活锁。
一、活锁的定义与原理
活锁(Livelock) 是并发编程中的问题:线程虽然没有被阻塞(仍在运行),但无法继续执行后续任务,因为彼此不断响应对方的动作,导致系统陷入“无限循环”的无效操作中。
sequenceDiagram
participant T1 as Thread1
participant T2 as Thread2
T1->>T1: 获取Lock1
T2->>T2: 获取Lock2
T1->>T2: 尝试获取Lock2(失败)
T2->>T1: 尝试获取Lock1(失败)
T1->>T1: 释放Lock1
T2->>T2: 释放Lock2
T1->>T1: 重新获取Lock1
T2->>T2: 重新获取Lock2
loop 活锁循环
T1->>T2: 尝试获取Lock2(失败)
T2->>T1: 尝试获取Lock1(失败)
T1->>T1: 释放Lock1
T2->>T2: 释放Lock2
end
经典场景:
- 两人在走廊相遇,反复避让导致路径持续阻塞。
- 线程A和线程B互相释放资源并重试,导致循环冲突。
二、活锁与死锁的区别
- 死锁:线程互相等待资源,完全停止执行。
- 活锁:线程持续执行,但无法推进任务。
1. 死锁(Deadlock)示意图
graph TD
subgraph "死锁:线程互相等待资源"
T1[线程1] -->|持有资源A,请求资源B| R1[资源A]
T2[线程2] -->|持有资源B,请求资源A| R2[资源B]
T1 -.->|等待资源B| T2
T2 -.->|等待资源A| T1
end
关键特征:
- 线程互相持有对方需要的资源,且不释放。
- 所有线程被永久阻塞(不再运行)。
- 形成环形等待链(如
T1 → T2 → T1)。
2. 活锁(Livelock)示意图
graph TD
subgraph "活锁:线程反复释放资源"
T1 -->|尝试获取资源B| R2[资源B]
T2[线程2] -->|释放资源B| R2[资源B]
T2 -->|尝试获取资源A| R1[资源A]
T1[线程1] -->|释放资源A| R1[资源A]
T1 -.->|失败后重试| T2
T2 -.->|失败后重试| T1
end
关键特征:
- 线程主动释放资源并重试,但重试策略同步。
- 线程仍在运行,但任务无进展。
- 形成无限循环的无效操作。
对比总结(表格形式)
| 特征 | 死锁(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顺序)。 - 避免对称操作:禁止线程同时释放资源并重试。
- 退避策略集成:在锁机制中内置随机退避(如数据库事务的指数退避算法)。
总结
这些问题覆盖了活锁的核心概念、实际应用和解决方案,是面试中考察并发编程能力的典型题目。回答时需结合代码示例或系统设计经验以体现深度。

浙公网安备 33010602011771号