阐述CAS的原理

753 阅读5分钟

知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方可以评论,我们一起探讨!

CAS(Compare-And-Swap)深度解析

CAS(Compare-And-Swap)是一种 无锁原子操作,通过硬件指令直接支持多线程环境下的并发修改,是实现高性能无锁数据结构(如无锁队列、原子计数器)的核心机制。以下是其原理、实现细节、应用场景及局限性的全面分析。


一、CAS 的核心原理

1. 操作语义

CAS 包含三个操作数:

  • 内存位置(V):需要更新的共享变量地址。
  • 预期原值(A):线程认为当前内存中的值。
  • 新值(B):希望将内存值更新为的目标值。

执行逻辑

image.png

  • 原子性:整个操作由 CPU 硬件指令保证不可分割。

2. 硬件支持

  • x86 架构:通过 LOCK CMPXCHG 指令实现。
    • LOCK 前缀:锁定总线或缓存行,确保独占访问。
    • CMPXCHG:比较并交换(Compare and Exchange)。
  • ARM 架构:通过 LL/SC(Load-Linked/Store-Conditional)指令对实现。
    • LDREX(Load-Exclusive):加载内存值并标记内存区域。
    • STREX(Store-Exclusive):仅在标记未失效时写入新值。

3. Java 中的 CAS

Java 通过 sun.misc.Unsafe 类的本地方法调用硬件指令:

public final class Unsafe {
    public final native boolean compareAndSwapInt(
        Object o, long offset, int expected, int x
    );
}
  • 原子类示例AtomicInteger 的实现:
    public class AtomicInteger {
        private volatile int value; // volatile 保证可见性
        public final boolean compareAndSet(int expect, int update) {
            return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
        }
    }
    

二、CAS 的典型应用场景

1. 无锁计数器

public class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        int oldValue;
        do {
            oldValue = count.get();
        } while (!count.compareAndSet(oldValue, oldValue + 1));
    }
}
  • 优势:避免锁竞争,高并发下性能优于 synchronized

2. 自旋锁(Spin Lock)

public class SpinLock {
    private AtomicBoolean locked = new AtomicBoolean(false);

    public void lock() {
        while (!locked.compareAndSet(false, true)) {
            // 自旋等待
        }
    }

    public void unlock() {
        locked.set(false);
    }
}
  • 适用场景:临界区代码极短,竞争不激烈。

3. 无锁队列(Disruptor 框架)

  • 核心机制:通过 CAS 更新队列头尾指针,实现多生产者/消费者的无锁并发。

三、CAS 的底层实现细节

1. x86 的 CMPXCHG 指令

; 伪代码示例:
mov eax, A        ; 预期原值 A
mov ebx, B        ; 新值 B
lock cmpxchg [V], ebx  ; 原子比较并交换
; 若 [V] == eax,则 [V] = ebx,ZF(Zero Flag)= 1
; 否则,ZF = 0
  • LOCK 前缀的作用
    • 锁定总线(早期 CPU)或缓存行(现代 CPU 的缓存一致性协议 MESI),确保操作原子性。

2. ARM 的 LL/SC 指令对

; 伪代码示例:
LL: load-exclusive [V], regA  ; 加载并标记内存区域
cmp regA, A                   ; 比较当前值与预期原值
bne FAIL                      ; 不相等则跳转
SC: store-exclusive [V], B, regB  ; 尝试写入新值
cmp regB, 0                   ; 检查是否写入成功
beq SUCCESS                   ; 成功则跳转
FAIL: retry or abort
  • 标记失效条件:其他线程修改了标记的内存区域。

四、CAS 的局限性

1. ABA 问题

  • 问题描述
    线程 1 读取内存值 A,线程 2 将 A→B→A,线程 1 的 CAS 仍会成功,但中间状态已被修改。
  • 解决方案
    • 版本号标记:使用 AtomicStampedReferenceAtomicMarkableReference
    • 时间戳:每次更新递增版本号。
    public class AtomicStampedReference<V> {
        private volatile Pair<V> pair;
        public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) {
            // 同时比较引用和版本号
        }
    }
    

2. 自旋开销

  • 高竞争场景
    线程长时间自旋重试,浪费 CPU 资源。
  • 优化策略
    • 退避算法:失败后增加等待时间(如指数退避)。
    • 适应性自旋:JVM 根据历史成功率动态调整自旋次数。

3. 单变量原子性

  • 复合操作限制
    CAS 只能保证单个变量的原子性,多变量操作需结合其他机制(如事务内存)。

五、CAS 与锁的性能对比

场景CAS(无锁)锁(synchronized/ReentrantLock)
低竞争性能高(无上下文切换)性能中等(存在锁获取/释放开销)
高竞争性能差(自旋浪费 CPU)性能稳定(线程阻塞,减少 CPU 浪费)
临界区执行时间适合极短操作(纳秒级)适合较长操作(微秒级以上)

六、CAS 的最佳实践

  1. 避免过度使用:仅在低竞争或短临界区场景使用 CAS。
  2. 结合版本号(AtomicStampedReference):解决 ABA 问题。
  3. 监控自旋次数:在高竞争时退化为锁机制。
  4. 伪共享优化:对频繁 CAS 的变量填充缓存行。
    class PaddedAtomicLong {
        volatile long value;
        long p1, p2, p3, p4, p5, p6, p7; // 填充至 64 字节
    }
    

七、CAS 在并发框架中的应用

1. Java 并发包(JUC)

  • AtomicXXX:如 AtomicIntegerAtomicReference
  • ConcurrentHashMap:通过 CAS 实现无锁化的 putIfAbsentcompute 等方法。

2. Disruptor 框架

  • 环形缓冲区:生产者通过 CAS 更新 cursor,消费者通过 CAS 确认位置。

3. Netty 的 ByteBuf

  • 引用计数:通过 AtomicIntegerFieldUpdater 实现无锁的引用计数管理。

总结

CAS 通过硬件指令直接支持无锁编程,显著提升了高并发场景下的性能,但其适用性受限于 ABA 问题、自旋开销和单变量原子性。合理使用 CAS 需结合场景特点,辅以版本号、退避策略和性能监控工具(如 JMH)。理解其底层硬件机制(如 CMPXCHGLL/SC)和内存模型(如 MESI)是优化无锁代码的关键。