概念解析

悲观锁 / 乐观锁

特性悲观锁乐观锁
核心思想认为并发冲突很可能发生,先加锁再操作数据。认为冲突较少发生,直接操作数据,提交时检查冲突。
实现方式synchronizedReentrantLock、数据库行锁。CAS、版本号机制(如数据库 version 字段)。
适用场景写操作频繁、竞争激烈。读多写少、低竞争环境。
性能开销锁管理(获取/释放)开销大。无锁,但冲突重试可能增加开销。

1. CAS(Compare-And-Swap)

  • 定义
    CAS 是一种无锁(Lock-Free)的原子操作,用于在多线程环境中实现同步。它的操作逻辑是:
    只有当变量的当前值与预期值一致时,才将其更新为新值。整个过程是原子性的,确保线程安全。是 乐观锁

  • 伪代码表示

    boolean CAS(V, expected, newValue) {
        if (V.value == expected) {
            V.value = newValue;
            return true;
        }
        return false;
    }
  • 应用场景

    • 实现无锁数据结构(如无锁队列、栈)。
    • Java 中的 AtomicIntegerAtomicReference 等原子类。
    • 数据库乐观锁(通过版本号机制)。
  • 优缺点

    • ✅ 避免锁开销,适用于低竞争场景。
    • ❌ 高竞争时可能导致大量自旋(CPU 空转)。
    • ABA 问题(可通过版本号或标记解决)。

2. 自旋锁(Spin Lock)

  • 定义
    线程在尝试获取锁时,若锁已被占用,会通过循环(自旋)不断检查锁状态,而非立即阻塞。
    属于悲观锁的一种实现(默认会有竞争,需主动获取锁)。

  • 适用场景
    锁持有时间极短(如内核短临界区),避免线程切换的开销。

  • 伪代码示例

    while (!tryAcquireLock()) {
        // 自旋等待,占用 CPU
    }
  • 优缺点

    • ✅ 无上下文切换,响应快。
    • ❌ 占用 CPU 资源,不适合长时间阻塞。

3. 互斥锁(Mutex Lock)

  • 定义
    线程获取锁失败时,会进入阻塞状态(让出 CPU),等待锁释放后被唤醒。
    也属于悲观锁,但通过线程休眠避免 CPU 浪费。

  • 适用场景
    锁持有时间较长或竞争激烈的情况。

  • 伪代码示例

    acquireLock();
    try {
        // 操作共享资源
    } finally {
        releaseLock();
    }
  • 优缺点

    • ✅ 不占用 CPU 资源。
    • ❌ 线程切换带来额外开销。

4. 读写锁(Read-write Lock)


总结

  • CAS:乐观锁的基石,通过原子性比较-交换避免锁开销,需处理 ABA 问题。
  • 自旋锁:悲观锁的一种,通过忙等待减少切换开销,适合短临界区。
  • 互斥锁:悲观锁的典型,通过阻塞线程节省 CPU,适合长临界区。
  • 选择策略
    • 低竞争、快速操作 → 乐观锁(CAS)
    • 高竞争、短临界区 → 自旋锁
    • 高竞争、长临界区 → 互斥锁

原子化

cmpxchg: Compare and Exchange

通过 EAX 寄存器存储预期值,并与内存或寄存器中的目标值比较,若相等则交换为新值。

原子化操作 = 加锁

条件变量

  • 通常,临界区代码只有在特定条件满足时才需要执行。
  • 这时可以使用条件变量:
    • 线程先获取临界区的锁,然后调用条件变量等待条件成立。
    • 等待线程会原子性地释放锁并进入休眠状态。
    • 若有多个等待线程,它们将被放入队列。
    • 当收到 signal_all 信号时,其中一个被唤醒的线程会重新获取锁。
  • 这种方式比不断轮询锁状态以检查条件是否满足更高效。
  • wait(cond, lock)
    • 原子性地释放锁并休眠。唤醒时尝试重新获取锁。
  • signal(cond)
    • 唤醒一个在 cond 上等待的休眠线程。
  • signal_all(cond)
    • 唤醒所有在 cond 上等待的休眠线程。