返回文章列表

文章

锁的常见问题、根本原因及对应的避免策略

在并发编程中,使用锁(如 synchronized、ReentrantLock 等)是实现线程同步的常见手段,但也可能引发一系列问题

目录
  1. 在并发编程中,使用锁(如 synchronized、ReentrantLock 等)是实现线程同步的常见手段,但也可能引发一系列问题。以下是锁的常见问题、根本原因及对应的避免策略:
  2. 一、锁引发的典型问题
  3. 1. 死锁(Deadlock)
  4. 2. 活锁(Livelock)
  5. 3. 锁竞争(Lock Contention)
  6. 4. 优先级反转(Priority Inversion)
  7. 5. 资源泄漏(Resource Leak)
  8. 二、避免锁问题的策略
  9. 1. 死锁的预防与解决
  10. 2. 活锁的解决
  11. 3. 减少锁竞争
  12. 4. 避免优先级反转
  13. 5. 防止资源泄漏
  14. 6. 替代锁的并发模型
  15. 三、典型问题代码 vs 优化方案
  16. 问题代码(死锁风险)
  17. 优化方案(统一锁顺序)
  18. 通过合理选择锁策略、减少锁的依赖,以及采用更高级的并发模型,可以有效避免锁带来的问题,提升程序的健壮性和性能。 以下是一个典型的活锁(Livelock)代码示例及其详细解析。活锁的特点是线程并未被阻塞,但因相互“礼让”或重复无效操作,导致程序无法继续执行。
  19. 活锁示例(Java实现)
  20. 活锁 vs 死锁
  21. 解决活锁的方案
  22. 修正后的代码(解决活锁)
  23. 总结
  24. 四、最佳实践总结

在并发编程中,使用锁(如 synchronizedReentrantLock 等)是实现线程同步的常见手段,但也可能引发一系列问题。以下是锁的常见问题、根本原因及对应的避免策略:#

一、锁引发的典型问题#

1. 死锁(Deadlock)#

  • 现象:多个线程互相等待对方释放锁,导致程序永久停滞。
  • 必要条件
    • 互斥(Mutual Exclusion)
    • 持有并等待(Hold and Wait)
    • 不可剥夺(No Preemption)
    • 循环等待(Circular Wait)
  • 示例

// 线程1 synchronized (lockA) { synchronized (lockB) { ... } }

// 线程2 synchronized (lockB) { synchronized (lockA) { ... } } ```

2. 活锁(Livelock)#

  • 现象:线程不断重试某个操作(如重试获取锁),但始终无法向前推进。
  • 原因:线程间的“过度礼貌”行为(例如反复让出资源)。
  • 示例:两个线程在走廊相遇,互相让路后又同时移动到同一侧,反复循环。

3. 锁竞争(Lock Contention)#

  • 现象:多个线程频繁竞争同一把锁,导致性能下降。
  • 原因:锁粒度过大(如全局锁)或高并发场景下的频繁同步。

4. 优先级反转(Priority Inversion)#

  • 现象:低优先级线程持有锁,导致高优先级线程被迫等待。
  • 经典案例:火星探路者号因优先级反转导致系统重启。

5. 资源泄漏(Resource Leak)#

  • 现象:未正确释放锁(如异常未处理),导致后续线程永久阻塞。
  • 示例:在临界区抛出异常后未释放锁。

二、避免锁问题的策略#

1. 死锁的预防与解决#

  • 统一锁的获取顺序:所有线程按固定顺序获取锁(如先 lockAlockB)。
  • 超时机制:尝试获取锁时设置超时(如 tryLock(timeout)),超时后回退并重试。
  • 死锁检测:定期检查线程和锁的依赖关系,强制释放锁或终止线程(需额外算法支持)。

2. 活锁的解决#

  • 随机退避(Backoff):在重试时引入随机等待时间(避免同步重试)。
  • 协调机制:使用中央调度器分配资源(如通过队列管理请求)。

3. 减少锁竞争#

  • 缩小锁粒度:用更细粒度的锁(如分段锁)替代全局锁。

// 分段锁示例(类似ConcurrentHashMap) Striped stripedLocks = Striped.lock(16); Lock lock = stripedLocks.get(key); lock.lock();

```
  • 无锁编程(Lock-Free):使用原子操作(如 AtomicInteger)或CAS(Compare-And-Swap)。

AtomicInteger counter = new AtomicInteger(); counter.incrementAndGet(); // 无锁自增

```
  • 读写锁分离:读操作共享锁,写操作独占锁(ReentrantReadWriteLock)。

4. 避免优先级反转#

  • 优先级继承(Priority Inheritance):临时提升持有锁的低优先级线程的优先级。
  • 无锁设计:使用无锁数据结构或事务内存(如STM)。

5. 防止资源泄漏#

  • RAII模式:在作用域结束时自动释放锁(如Java的 try-with-resources)。

try (LockScope scope = new LockScope(lock)) { // 临界区操作 } // 自动释放锁

```
  • try-finally块:确保锁在异常时仍被释放。

lock.lock(); try { // 临界区操作 } finally { lock.unlock(); }

```

6. 替代锁的并发模型#

  • 消息传递(Actor/CSP):通过消息队列或Channel通信,避免共享状态。

// Go的Channel实现生产者-消费者(无显式锁) ch := make(chan int, 5) go func() { ch <- data }() // 生产者 data := <-ch // 消费者

```
  • 不可变数据(Immutable Data):共享数据只读,无需锁。

public class ImmutableData { private final int value; public ImmutableData(int value) { this.value = value; } public int getValue() { return value; } // 只读,无需同步 }

```


三、典型问题代码 vs 优化方案#

问题代码(死锁风险)#

// 线程1
synchronized (lockA) {
    synchronized (lockB) { ... }
}

// 线程2
synchronized (lockB) {
    synchronized (lockA) { ... }
}

优化方案(统一锁顺序)#

// 所有线程统一先获取lockA,再获取lockB
synchronized (lockA) {
    synchronized (lockB) { ... }
}


通过合理选择锁策略、减少锁的依赖,以及采用更高级的并发模型,可以有效避免锁带来的问题,提升程序的健壮性和性能。 以下是一个典型的活锁(Livelock)代码示例及其详细解析。活锁的特点是线程并未被阻塞,但因相互“礼让”或重复无效操作,导致程序无法继续执行。#

活锁示例(Java实现)#

场景描述 两个线程(线程A和线程B)需要同时获取两个资源(ResourceA和ResourceB)才能工作。当无法同时获取两个资源时,它们会释放已持有的资源并重试。由于释放和重试的节奏相同,导致无限循环。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LivelockDemo {
    private static final Lock resourceA = new ReentrantLock();
    private static final Lock resourceB = new ReentrantLock();

    public static void main(String[] args) {
        Thread threadA = new Thread(() -> {
            while (true) {
                if (resourceA.tryLock()) { // 尝试获取资源A
                    try {
                        System.out.println("线程A 获取了资源A");
                        if (resourceB.tryLock()) { // 尝试获取资源B
                            try {
                                System.out.println("线程A 成功获取资源A和B!开始工作...");
                                return; // 完成任务后退出
                            } finally {
                                resourceB.unlock();
                            }
                        }
                    } finally {
                        resourceA.unlock(); // 释放资源A
                    }
                }
                // 等待一段时间后重试
                try { Thread.sleep(10); } catch (InterruptedException e) {}
            }
        });

        Thread threadB = new Thread(() -> {
            while (true) {
                if (resourceB.tryLock()) { // 尝试获取资源B
                    try {
                        System.out.println("线程B 获取了资源B");
                        if (resourceA.tryLock()) { // 尝试获取资源A
                            try {
                                System.out.println("线程B 成功获取资源A和B!开始工作...");
                                return; // 完成任务后退出
                            } finally {
                                resourceA.unlock();
                            }
                        }
                    } finally {
                        resourceB.unlock(); // 释放资源B
                    }
                }
                // 等待一段时间后重试
                try { Thread.sleep(10); } catch (InterruptedException e) {}
            }
        });

        threadA.start();
        threadB.start();
    }
}


运行结果分析

线程A 获取了资源A
线程B 获取了资源B
线程A 获取了资源A
线程B 获取了资源B
线程A 获取了资源A
线程B 获取了资源B
...(无限循环,无法完成任务)...

活锁发生过程

  1. 线程A 获取资源A,尝试获取资源B(失败)→ 释放资源A → 等待10ms后重试。
  2. 线程B 获取资源B,尝试获取资源A(失败)→ 释放资源B → 等待10ms后重试。
  3. 由于两个线程的重试节奏完全同步,导致它们反复获取部分资源后释放,永远无法同时持有两个资源。

活锁 vs 死锁#

**特征****活锁(Livelock)****死锁(Deadlock)**
线程状态线程仍在运行(未阻塞)线程被阻塞(等待锁)
资源持有可能释放已获取的资源持有资源并等待其他资源
表现形式程序看似运行,但无实际进展程序完全停滞
触发条件重试策略设计不当锁的循环等待

解决活锁的方案#

  1. 引入随机退避(Random Backoff) 在重试时增加随机等待时间,打破同步节奏:
// 修改重试代码(两个线程的退避时间不同)
try {
    Thread.sleep((long) (Math.random() * 100)); // 随机等待0~100ms
} catch (InterruptedException e) {}

  1. 统一资源获取顺序 强制所有线程按相同顺序获取资源:
// 线程B修改为和线程A相同的获取顺序(先A后B)
if (resourceA.tryLock()) { // 先尝试获取A
    try {
        if (resourceB.tryLock()) { // 再尝试获取B
            // ...
        }
    } finally {
        resourceA.unlock();
    }
}

  1. 限制最大重试次数 超过重试次数后终止线程或抛出异常:
int retryCount = 0;
while (retryCount++ < 10) { // 最多重试10次
    // 尝试获取资源...
}
throw new RuntimeException("活锁无法解决");


修正后的代码(解决活锁)#

// 在重试时增加随机退避
try {
    Thread.sleep((long) (Math.random() * 100)); // 随机等待0~100ms
} catch (InterruptedException e) {}

// 统一资源获取顺序(两个线程都先获取A再获取B)


总结#

活锁的本质是线程间的无效协作,解决方案的核心是打破同步性:

  1. 随机退避:避免线程步调一致。
  2. 统一资源顺序:消除循环依赖。
  3. 限制重试:防止无限循环。 实际开发中,活锁较难调试,可通过日志记录重试次数和资源状态来辅助诊断。

四、最佳实践总结#

  1. 优先使用高级工具
    • 使用线程安全的集合(如 ConcurrentHashMap)替代手动加锁。
    • 利用并发框架(如 ExecutorServiceForkJoinPool)。
  2. 减少锁的范围
    • 仅在必要的最小代码段内加锁,避免长时间持有锁。
  3. 避免嵌套锁
    • 尽量使用单锁,减少多锁交互的复杂性。
  4. 测试与监控
    • 使用工具检测死锁(如Java的 jstackVisualVM)。
    • 压力测试验证高并发下的锁竞争情况。