基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

CAS原理及应用

知识点图片

本文讲解CAS(比较并交换)原理:一种乐观锁技术,通过原子指令实现无锁并发。它在更新变量时,先比较内存值是否与预期值相等,若相等则更新,否则重试。它性能高,但存在ABA问题和高并发下的CPU消耗。

我们来深入浅出地讲解一下 CAS 的原理。

CAS(Compare-and-Swap,比较并交换)是现代 CPU 广泛支持的一种原子指令。它是一种实现并发算法时用到的技术,通常被认为是乐观锁(Optimistic Locking)的一种实现方式。


1. 核心思想:先比较,再交换

CAS 操作包含三个核心操作数:

  1. V (Variable): 内存地址中的变量值。
  2. E (Expected): 预期的旧值。
  3. N (New): 准备要更新的新值。

执行过程如下:

当一个线程准备更新变量 V 时,它会执行一个 CAS 操作。这个操作是原子的,意味着它在执行过程中不会被其他线程打断。

  1. CPU 会首先读取内存地址 V 处的当前值。
  2. 然后,将这个当前值与线程提供的预期值 E 进行比较。
  3. 如果 当前值与预期值 E 相等,说明从上次读取到准备写入的这段时间里,没有其他线程修改过这个变量。这时,CPU 就会将内存地址 V 的值更新为新值 N。操作成功。
  4. 如果 当前值与预期值 E 不相等,说明这个变量已经被其他线程修改了。这时,CPU 不会执行任何更新操作。操作失败。

整个“比较并交换”的过程由硬件保证其原子性,你不需要担心在这个过程中出现竞态条件。


2. 生活中的比喻

想象一下你在网上抢购最后一张演唱会门票:

  1. 读取库存: 你看到系统显示“库存:1”。(这相当于读取内存值 V,此时你的预期值 E 是 1)。
  2. 准备下单: 你准备提交订单,要把库存减到 0。(你的新值 N 是 0)。
  3. 提交(CAS操作): 系统在处理你的订单时,会进行一次 CAS 操作:
    • 检查库存是否还是 1? (比较 VE)
    • 如果是,说明没人跟你抢,系统就把库存更新为 0。你抢票成功!
    • 如果不是(比如变成了 0),说明就在你提交订单的瞬间,另一个人手速更快,已经买走了。你的操作失败,系统会提示你“票已售罄”。

3. CAS 的应用:自旋锁(Spin Lock)

单独一次 CAS 操作如果失败了,通常不会就此罢休。在实际应用中,CAS 通常与一个循环结合使用,形成一种“自旋”或“重试”的机制。

基本模式(伪代码):

plaintext
do {
    // 1. 读取内存中的当前值
    oldValue = memory.get();

    // 2. 根据当前值计算出新值
    newValue = calculateNewValue(oldValue);

    // 3. 尝试用CAS更新
    // 如果内存值仍然是oldValue,就更新为newValue
    // success 会返回 true 或 false
    success = cas(memory, oldValue, newValue);

} while (!success); // 如果不成功,就不断循环重试

这个循环过程就是所谓的自旋。线程不会被挂起(像 synchronized 那样),而是在原地“打转”,不断尝试更新,直到成功为止。

Java 中的例子:

Java 的 java.util.concurrent.atomic 包下的原子类,如 AtomicIntegerAtomicLongAtomicReference 等,都是基于 CAS 实现的。

AtomicIntegerincrementAndGet() 方法为例,其内部实现(简化后)就是这样的:

java
public class AtomicInteger {
    private volatile int value; // value 必须是 volatile 的,保证可见性

    public final int incrementAndGet() {
        for (;;) { // 无限循环,即“自旋”
            int current = get(); // 1. 读取当前值
            int next = current + 1; // 2. 计算新值
            if (compareAndSet(current, next)) { // 3. CAS操作
                return next; // 如果成功,返回新值
            }
            // 如果失败,循环会继续,重新读取、计算、尝试CAS
        }
    }

    // compareAndSet 是一个native方法,底层调用CPU指令
    public final native boolean compareAndSet(int expect, int update);
}

4. CAS 的优缺点

优点(为什么用它?):

  1. 高性能/乐观锁: 在并发冲突不激烈的情况下,线程不需要被挂起和唤醒。传统的锁(如 synchronized)涉及线程的上下文切换,开销较大。CAS 是一种“无锁”操作,线程可以一直保持运行状态,因此性能很高。
  2. 避免死锁: 由于没有“锁定”资源,只是在更新时进行比较,所以天然地避免了死锁问题。

缺点(有什么问题?):

  1. ABA 问题(重要):

    • 问题描述: 一个变量的值原来是 A,变成了 B,然后又变回了 A。当一个线程用 CAS 去检查时,它发现值仍然是 A(符合预期),于是执行了更新。但实际上,这个值已经被改变过了。在某些场景下,这会导致严重的问题。
    • 例子: 你有 100 元,想取 50 元。你读取到余额是 100。在你准备操作时,有人给你转了 50 元(余额变 150),然后又取走了 50 元(余额变回 100)。你再执行 CAS 时,发现余额还是 100,操作成功,余额变成 50。但实际上,账户发生过变动,你可能错过了某些中间状态。
    • 解决方案: 版本号机制。在变量旁边增加一个版本号。每次变量更新时,版本号也加一。CAS 操作不仅要比较值,还要比较版本号。Java 中的 AtomicStampedReference 就是用来解决 ABA 问题的,它将值和“戳(stamp)”绑定在一起。
  2. CPU 开销大: 如果并发冲突非常激烈,会导致大量线程反复“自旋”重试,这会持续消耗 CPU 资源,但并没有做有效工作。在这种情况下,性能可能还不如让线程挂起的传统锁。

  3. 只能保证单个共享变量的原子操作: CAS 只能对一个内存地址的变量进行原子操作。如果需要同时对多个变量进行原子更新,就需要使用更复杂的机制,比如将多个变量封装成一个对象,然后使用 AtomicReference 来对这个对象进行 CAS 操作,或者直接使用 synchronizedLock


总结

特性 CAS (乐观锁) synchronized (悲观锁)
核心思想 假设没有冲突,直接尝试操作,如果失败再重试。 假设总有冲突,先获取锁,再执行操作。
性能 冲突少时性能高,无线程上下文切换。 冲突多时性能稳定,线程会休眠,让出CPU。
CPU消耗 冲突多时,自旋会持续消耗CPU。 线程挂起,不消耗CPU,但切换有开销。
适用场景 读多写少,并发冲突不激烈的场景。 写操作多,并发冲突激烈的场景。
主要问题 ABA问题,高并发下自旋的CPU开销。 可能导致死锁,锁的获取和释放有开销。

CAS 是构建现代高性能并发库(如 Java 的 JUC 包)的基石,理解它的原理对于深入学习并发编程至关重要。

00:00
00:00