CAS 的工作原理

CAS (Compare-and-Swap) 本身并不是一个独立的项目或软件,而是一种底层的硬件指令和并发编程概念

1. 核心概念

  • CAS 是一种原子操作: 它的 “比较” 和 “交换” 这两个动作是作为一个不可分割的整体执行的,要么都成功,要么都失败,不会出现中间状态。
  • CAS 是一种无锁操作(乐观锁): 它在操作过程中不会阻塞线程,而是通过不断重试来实现同步。
  • CAS 操作三个数:
    • 内存位置 (V): 要读取和修改的内存地址。
    • 预期原值 (A): 期望 V 处当前的值。
    • 新值 (B): 如果 V 处的值等于 A,则将 V 处的值更新为 B。

2. CPU 指令层面 (以 x86 架构为例)

CAS 的原子性是由 CPU 的硬件指令保证的。 在 x86 架构上,CAS 操作通常对应 cmpxchg 指令族(Compare and Exchange)。 不同的操作数类型对应不同的指令:

  • cmpxchg (操作数可以是 8 位、16 位、32 位或 64 位)
  • cmpxchg8b (操作数是 64 位)
  • cmpxchg16b (操作数是 128 位,需要 CPU 支持)

cmpxchg 指令的工作流程(以 32 位为例):

  1. 准备阶段:

    • 将预期原值 (A) 放入 EAX 寄存器。
    • 将新值 (B) 放入 EBX 或 ECX 寄存器(具体取决于指令的操作数)。
    • 将内存位置 (V) 的地址放入一个通用寄存器(例如,ESI 或 EDI)。
  2. 执行 cmpxchg 指令:

    • CPU 执行 cmpxchg 指令,格式通常是:cmpxchg 目标操作数, 源操作数
    • 目标操作数通常是一个内存地址(例如 [ESI],表示 ESI 寄存器中存储的地址)。
    • 源操作数通常是一个寄存器(例如 EBX)。
  3. 比较与交换:

    • cmpxchg 指令内部会进行以下操作(原子操作):
      1. 读取内存位置 (V) 的当前值(假设为 C)。
      2. 将 C 与 EAX 寄存器中的值 (A) 进行比较。
      3. 如果 C 等于 A:
        • 将 EBX 寄存器中的值 (B) 写入内存位置 (V)。
        • 将零标志位 (ZF) 设置为 1,表示比较相等。
      4. 如果 C 不等于 A:
        • 将内存位置 (V) 的值 © 加载到 EAX 寄存器中。
        • 将零标志位 (ZF) 设置为 0,表示比较不相等。
  4. 结果判断:

    • cmpxchg 指令执行完成后,程序可以通过检查零标志位 (ZF) 来判断 CAS 操作是否成功。
    • 如果 ZF = 1,表示 CAS 操作成功,内存位置 (V) 的值已经被更新为 B。
    • 如果 ZF = 0,表示 CAS 操作失败,内存位置 (V) 的值没有被修改,EAX 寄存器中现在存储的是 V 处的当前值。

关键点:

  • 原子性: cmpxchg 指令本身是原子的,CPU 会保证这个指令在执行过程中不会被中断。
  • 总线锁或缓存锁: 在多核处理器环境下,为了保证 cmpxchg 指令的原子性,CPU 会使用总线锁 (BUS Lock) 或缓存锁 (Cache Lock) 机制。
    • 总线锁: 在早期 CPU 中,cmpxchg 指令会在总线上发送一个 LOCK# 信号,锁定总线,阻止其他 CPU 访问内存,从而保证操作的原子性。 总线锁的开销较大。
    • 缓存锁: 现代 CPU 更多地使用缓存锁机制。 如果要操作的内存区域在 CPU 的缓存行 (Cache Line) 中,CPU 会锁定该缓存行,而不是锁定整个总线。 缓存锁的开销较小,可以提高多核处理器的性能。
  • EAX 寄存器: EAX 寄存器在 cmpxchg 指令中扮演着重要的角色,它既用于存储预期原值 (A),也用于在 CAS 操作失败时存储内存位置 (V) 的当前值。

3. Java 层面 (Unsafe 类)

Java 无法直接使用 CPU 指令,但可以通过 sun.misc.Unsafe 类间接使用。 Unsafe 类提供了一些 native 方法,可以调用底层的操作系统和硬件功能,包括 CAS 操作。

  • Unsafe 类中的 CAS 方法:

    public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
    public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
    public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);
    
    • o: 要操作的对象。
    • offset: 要操作的字段在对象 o 中的内存偏移量(可以通过 Unsafe.objectFieldOffset() 方法获取)。
    • expected: 预期原值 (A)。
    • x: 新值 (B)。
    • 返回值:boolean 类型,表示 CAS 操作是否成功。
  • Unsafe 类是 “unsafe” 的:

    • Unsafe 类的设计初衷是供 Java 核心库内部使用,而不是供普通开发者使用。
    • 直接使用 Unsafe 类可能会导致安全问题、可移植性问题和稳定性问题。
    • 在 Java 9 之后,Unsafe 类的访问受到更严格的限制。
    • 不要生产代码中直接使用 Unsafe 类!

4. Java 并发包中的原子类 (Atomic Classes)

Java 并发包 (java.util.concurrent.atomic) 提供了一系列原子类,它们内部使用了 Unsafe 类的 CAS 操作来实现原子操作,并且对开发者屏蔽了底层的复杂性。

  • 原子类的工作原理 (以 AtomicInteger 为例):

    public class AtomicInteger extends Number implements java.io.Serializable {
        private static final long serialVersionUID = 6214790243416807050L;
    
        // setup to use Unsafe.compareAndSwapInt for updates
        private static final Unsafe unsafe = Unsafe.getUnsafe();
        private static final long valueOffset;
    
        static {
            try {
                valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
            } catch (Exception ex) { throw new Error(ex); }
        }
    
        private volatile int value; // 使用 volatile 关键字修饰
    
        // ... 其他方法 ...
    
        public final boolean compareAndSet(int expect, int update) {
            return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
        }
    
        // ...
    }
    
    • value 字段:AtomicInteger 内部使用一个 volatile int value 字段来存储整数值。 volatile 关键字确保了 value 字段的可见性和有序性。
    • valueOffset 字段:valueOffsetvalue 字段在 AtomicInteger 对象中的内存偏移量,它在静态代码块中通过 Unsafe.objectFieldOffset() 方法获取。
    • compareAndSet() 方法:compareAndSet() 方法是 AtomicInteger 中实现 CAS 操作的核心方法。它直接调用 Unsafe.compareAndSwapInt() 方法来进行 CAS 操作。
      • this: 表示当前 AtomicInteger 对象。
      • valueOffset: value 字段的偏移量。
      • expect: 预期原值。
      • update: 新值。
  • 原子类的使用:

    • 开发者可以直接使用原子类提供的 API(例如 get(), set(), compareAndSet(), incrementAndGet() 等),而无需关心底层的 CAS 实现细节。
    • 原子类保证了这些操作的原子性、可见性和有序性。

5. CAS 与自旋

由于 CAS 操作在失败时不会阻塞线程,而是立即返回,因此通常需要在一个循环中不断重试 CAS 操作,直到成功为止。 这种循环重试的机制称为 “自旋”。

  • 自旋示例 (使用 AtomicInteger):

    AtomicInteger counter = new AtomicInteger(0);
    
    // 使用 CAS 实现原子递增
    public void increment() {
        int oldValue;
        int newValue;
        do {
            oldValue = counter.get(); // 获取当前值
            newValue = oldValue + 1;   // 计算新值
        } while (!counter.compareAndSet(oldValue, newValue)); // CAS 操作,循环重试直到成功
    }
    
    • increment() 方法中,使用 do-while 循环不断重试 CAS 操作。
    • 每次循环中,先获取 counter 的当前值 (oldValue),然后计算新值 (newValue)。
    • 调用 counter.compareAndSet(oldValue, newValue) 进行 CAS 操作。
      • 如果 CAS 操作成功,则退出循环。
      • 如果 CAS 操作失败(说明其他线程修改了 counter 的值),则继续循环重试。
  • 自旋的优化:

    • 自适应自旋: 根据历史 CAS 操作的成功率,动态调整自旋的次数。 如果 CAS 操作经常成功,则可以增加自旋次数;如果 CAS 操作经常失败,则可以减少自旋次数,甚至直接放弃自旋,转而使用锁或其他同步机制。
    • 退避策略: 在每次自旋失败后,增加一个短暂的延迟(例如使用 Thread.yield()LockSupport.parkNanos()),避免过多的 CPU 消耗。 延迟的时间可以逐渐增加,例如指数退避。
    • 限制自旋次数: 设置一个最大的自旋次数, 超出最大自旋次数就挂起线程。

总结

CAS 是一种基于 CPU 指令实现的无锁(乐观锁)原子操作。 它通过 “比较并交换” 内存位置的值来实现同步,避免了传统锁的阻塞和死锁问题。 CAS 的工作原理可以概括为:

  1. 读取内存值: 读取要操作的内存位置的当前值。
  2. 比较: 将当前值与预期原值进行比较。
  3. 交换: 如果当前值等于预期原值,则将内存位置的值更新为新值;否则,不进行任何操作。
  4. 原子性保证: 整个 “比较并交换” 过程由 CPU 指令保证原子性。
  5. 自旋重试: 如果 CAS 操作失败,通常会在一个循环中不断重试,直到成功为止。

在 Java 中,CAS 操作主要通过 sun.misc.Unsafe 类和 java.util.concurrent.atomic 包中的原子类来实现。 开发者应该优先使用原子类,而不是直接使用 Unsafe 类。 理解 CAS 的工作原理、优缺点和适用场景,可以帮助你更好地进行并发编程,提高程序的性能和可靠性。

参考资料

Java 并发编程相关文档 (Java 层面)

  • java.util.concurrent.atomic 包的 Javadoc:

    • 这是 Java 并发包中原子类的官方文档,其中介绍了原子类的使用方法,以及它们与 CAS 的关系。
    • 在线文档:
      • Java 8: https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html
      • Java 11: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/atomic/package-summary.html
      • Java 17: https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/atomic/package-summary.html
  • sun.misc.Unsafe 类 (不建议直接使用):

    • 虽然 Unsafe 类不是公开的 API,但你可以通过反射等方式查看其源码和 Javadoc (如果你有 OpenJDK 源码)。
    • Unsafe 类中的 compareAndSwapInt, compareAndSwapLong, compareAndSwapObject 等方法是 CAS 操作的底层实现。
    • 强烈不建议在生产代码中直接使用 Unsafe 类!
  • Java Language Specification (JLS) 和 Java Memory Model (JMM):

    • 这些文档定义了 Java 语言的规范和 Java 内存模型,与 CAS 操作的可见性、原子性、有序性密切相关。
    • 在线文档
      * JLS: https://docs.oracle.com/javase/specs/
      * JMM: 包含在JLS的第17章 https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html

你可能感兴趣的:(Java,开发,2025,Java面试系列,java)