Category 扭蛋工坊

大家对死锁都比较熟悉,今天来快速学习一下活锁。

一、活锁的定义与原理

活锁(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 顺序)。

避免对称操作:禁止线程同时释放资源并重试。

退避策略集成:在锁机制中内置随机退避(如数据库事务的指数退避算法)。

总结

这些问题覆盖了活锁的核心概念、实际应用和解决方案,是面试中考察并发编程能力的典型题目。回答时需结合代码示例或系统设计经验以体现深度。

top
Copyright © 2088 娜拉物语最新活动_二次元冒险手游官网 All Rights Reserved.
友情链接