三分钟掌握活锁(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...
...(无限循环)

四、活锁的检测与解决

检测方法

  1. 日志分析:观察日志中线程频繁重试但无进展。
  2. 性能监控:CPU占用高但任务无完成。

解决方案

  1. 随机退避:在重试时引入随机等待(如代码中获取资源时使用 sleep)。
  2. 优先级调度:为线程设置不同的重试优先级。
  3. 超时机制:限制最大重试次数后终止或回退。

五、总结

  • 活锁本质:线程因过度“礼貌”导致无效循环。
  • 避免关键:通过随机退避、超时或设计资源获取顺序打破对称性。
  • 实际应用:在分布式系统、数据库事务中广泛使用退避策略(如指数退避)。

六、活锁(Livelock)高频面试题

1. 活锁与死锁的区别是什么?请从线程状态、资源持有、系统表现三方面说明。

答案:

  • 线程状态
    活锁中的线程仍在运行(非阻塞),而死锁中的线程完全阻塞(停止运行)。
  • 资源持有
    活锁中线程会主动释放并重试获取资源,死锁中线程永久占用资源不释放。
  • 系统表现
    活锁导致高 CPU 占用但任务无进展,死锁导致 CPU 闲置且任务完全停滞。

2. 举一个活锁的实际场景或代码示例,并说明其成因。

答案

  • 场景
    两个线程互相释放资源并重试,例如:
    // 线程1:先获取锁A,尝试锁B → 失败 → 释放锁A → 重试  
    // 线程2:先获取锁B,尝试锁A → 失败 → 释放锁B → 重试  
    
  • 成因
    线程的重试策略完全对称(如同时释放和重试),导致无限循环。

3. 如何解决活锁问题?至少给出三种方案并说明原理。

答案

  1. 随机退避:在重试时加入随机延迟(如 Thread.sleep(random.nextInt(100))),打破对称性。
  2. 优先级调度:为线程设置不同的重试优先级(如固定顺序获取资源)。
  3. 超时机制:限制最大重试次数后终止或回退(如 retryCount > MAX_RETRY)。

4. 如何检测系统中的活锁?给出具体方法或工具。

答案

  • 日志分析:观察线程日志中频繁的“获取-释放-重试”循环。
  • 性能监控:高 CPU 使用率但任务完成率为零。
  • 工具检测
    • 使用 jstack 或 VisualVM 查看线程状态(RUNNABLE 但无进展)。
    • APM 工具(如 Arthas)监控方法调用频率。

5. 在设计并发系统时,如何预防活锁?请结合设计原则说明。

答案

  • 资源有序分配:统一资源获取顺序(如总是按 lock1 → lock2 顺序)。
  • 避免对称操作:禁止线程同时释放资源并重试。
  • 退避策略集成:在锁机制中内置随机退避(如数据库事务的指数退避算法)。

总结

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

posted @ 2025-03-12 01:25  皮皮是个不挑食的好孩子  阅读(328)  评论(0)    收藏  举报