什么是 CAS?

CAS(Compare and Swap,比较并交换)是一种并发编程中的乐观锁机制,用于实现多线程环境下的无锁同步。其核心思想是:在更新一个值之前,先比较该值是否与预期值相等,如果相等则更新,否则不做操作

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

  • 内存地址 V:要操作的变量地址
  • 预期值 A:期望变量当前的值
  • 新值 B:要更新的新值

操作逻辑:如果内存地址 V 中的值等于预期值 A,则将其更新为 B;否则不做任何操作。整个过程是原子的,由 CPU 指令直接支持。

Java 中的 CAS 实现

1. Unsafe 类中的 CAS 方法

Unsafe 类提供了多个 CAS 相关方法,以 compareAndSwapInt 为例:

public final native boolean compareAndSwapInt(
    Object var1,   // 操作的对象
    long var2,     // 变量在对象中的偏移量(内存地址)
    int var4,      // 预期值 A
    int var5       // 新值 B
);
  • 作用:修改对象 var1 中偏移量为 var2 的 int 类型变量,若当前值等于 var4,则更新为 var5,返回 true;否则返回 false
  • 底层:通过 native 方法调用 CPU 原子指令,确保操作的原子性。

进一步看 Unsafe 的 getAndAddInt 方法(JDK 源码):

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        // 1. 获取当前内存中的值 v(预期值 A)
        v = this.getIntVolatile(o, offset);
        // 2. 执行 CAS:若当前值仍为 v,则更新为 v + delta
        // 3. 若 CAS 失败(被其他线程修改),则循环重试
    } while (!this.compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

核心逻辑

  • 通过 getIntVolatile volatile 语义获取当前值(保证可见性)。
  • 循环执行 CAS 操作:若成功则退出,若失败(值被其他线程修改)则重新获取最新值并再次尝试。
  • 这种 "循环重试" 的机制称为自旋,是 CAS 实现无锁同步的关键。
2. 原子类中的 CAS 应用

Java 的 java.util.concurrent.atomic 包(简称AIO, 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式)下的原子类(如 AtomicInteger)就是基于 CAS 实现的。

以 AtomicInteger 的 incrementAndGet 方法(自增 1 并返回新值)为例:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
    
    // 存储实际值,使用 volatile 保证可见性
    private volatile int value;
    
    // 获取 Unsafe 实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    
    // value 变量在类中的内存偏移量
    private static final long valueOffset;
    
    static {
        try {
            // 计算 value 字段的偏移量
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) {
            throw new Error(ex);
        }
    }
    
    public final int incrementAndGet() {
        // 调用 Unsafe 的 getAndAddInt 实现自增
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
}

CAS 的核心依赖:Unsafe 类

private static final Unsafe unsafe = Unsafe.getUnsafe();
  • Unsafe是 Java 提供的 "后门" 工具类,它能直接操作内存和执行底层 CAS 指令
  • CAS 的原子性正是通过Unsafe类的 native 方法实现的(这些方法直接映射到 CPU 的原子指令)

CAS 操作的三个关键要素:

(1)内存地址 V:valueOffset
  • valueOffset记录了value字段在AtomicInteger对象中的内存偏移量
  • 作用:相当于 CAS 中的 "内存地址 V",告诉 CPU 要操作的变量在内存中的具体位置
(2)预期值 A:当前内存中的值
private volatile int value;
  • value是实际存储整数的变量,被volatile修饰
  • 作用:保证多线程环境下的可见性,使得线程能获取到变量的最新值(作为 CAS 的 "预期值 A")
(3)新值 B:要更新的值
public final int incrementAndGet() {
    // 调用Unsafe的getAndAddInt实现自增
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
  • 这里的1就是要增加的量,最终会计算出 "新值 B"(当前值 + 1)

但其存在以下主要缺点:

  1. ABA 问题当一个值从 A 被修改为 B,再改回 A 时,CAS 会误认为该值未被修改而成功执行操作,这在某些场景下可能导致数据不一致。

    public class ABADemo {
        public static void main(String[] args) {
            AtomicInteger atomicInteger = new AtomicInteger(100);
            
            // 线程1:将100 -> 101 -> 100
            new Thread(() -> {
                atomicInteger.compareAndSet(100, 101);
                atomicInteger.compareAndSet(101, 100);
                System.out.println("线程1执行完毕");
            }).start();
            
            // 线程2:尝试将100 -> 200
            new Thread(() -> {
                try {
                    Thread.sleep(1000); // 等待线程1完成ABA操作
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean result = atomicInteger.compareAndSet(100, 200);
                System.out.println("线程2修改结果:" + result); // 输出true,误以为值未变
            }).start();
        }
    }

    线程 2 看到的值虽然还是 100,但这个值已经被线程 1 修改过再改回,CAS 无法感知这种中间变化,导致误判。
    解决方式:使用AtomicStampedReference,通过引入 "版本号"(标志),更新时不仅检查值是否匹配,还需验证版本号是否符合预期,避免 ABA 漏洞。

    public class SolveABADemo {
        public static void main(String[] args) {
            // 初始化值为100,版本号为1
            AtomicStampedReference<Integer> stampedRef = new AtomicStampedReference<>(100, 1);
            
            // 线程1:100(版本1) -> 101(版本2) -> 100(版本3)
            new Thread(() -> {
                int stamp = stampedRef.getStamp(); // 获取当前版本
                System.out.println("线程1初始版本:" + stamp);
                
                // 第一次修改:100->101,版本+1
                stampedRef.compareAndSet(100, 101, stamp, stamp + 1);
                stamp = stampedRef.getStamp(); // 更新版本号
                
                // 第二次修改:101->100,版本+1
                stampedRef.compareAndSet(101, 100, stamp, stamp + 1);
                System.out.println("线程1最终版本:" + stampedRef.getStamp());
            }).start();
            
            // 线程2:尝试用初始版本修改100->200
            new Thread(() -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int stamp = stampedRef.getStamp(); // 获取当前版本(实际已变为3)
                System.out.println("线程2获取的版本:" + stamp);
                
                // 验证值和版本号,此时版本不匹配,修改失败
                boolean result = stampedRef.compareAndSet(100, 200, 1, 2);
                System.out.println("线程2修改结果:" + result); // 输出false
            }).start();
        }
    }
  2. 循环时间长,CPU 开销大CAS 采用自旋方式(循环重试)直到操作成功,若并发冲突频繁导致长时间重试,会持续占用 CPU 资源,降低系统性能。
     

    public class SpinCostDemo {
        private static final AtomicInteger count = new AtomicInteger(0);
        
        public static void main(String[] args) {
            // 高并发场景下模拟大量CAS重试
            Runnable task = () -> {
                for (int i = 0; i < 100000; i++) {
                    count.incrementAndGet(); // 内部执行CAS自旋
                }
            };
            
            // 创建大量线程模拟高并发
            ExecutorService executor = Executors.newFixedThreadPool(20);
            long start = System.currentTimeMillis();
            
            for (int i = 0; i < 20; i++) {
                executor.submit(task);
            }
            
            executor.shutdown();
            while (!executor.isTerminated()) {}
            
            long end = System.currentTimeMillis();
            System.out.println("结果:" + count.get() + ",耗时:" + (end - start) + "ms");
        }
    }

    在高并发下,incrementAndGet()内部的 CAS 操作会频繁失败并重试(自旋),导致大量 CPU 资源被消耗在循环等待上。
    优化思路
    1. 降低并发冲突:通过分段锁(如LongAdder的实现)
    2. 限制自旋次数:超过阈值后阻塞(如ConcurrentHashMap的实现)
    3. 改用其他同步机制:在冲突频繁时使用synchronized

  3. 仅支持单个共享变量的原子操作
    CAS 只能保证对一个共享变量的操作原子性,无法直接处理多个变量的原子性场景。解决方式:可通过AtomicReference将多个变量封装为一个对象进行操作,或使用synchronized等锁机制实现多变量的原子性控制。
     

    // 将多个变量封装为对象,通过AtomicReference保证原子性
    public class MultiVariableSolution {
        // 封装多个变量为一个对象
        static class Data {
            int a;
            int b;
            
            Data(int a, int b) {
                this.a = a;
                this.b = b;
            }
        }
        
        private static final AtomicReference<Data> dataRef = new AtomicReference<>(new Data(0, 0));
        
        // 原子更新多个变量
        public static void update(int aNew, int bNew) {
            Data oldData;
            do {
                oldData = dataRef.get(); // 获取当前数据
                Data newData = new Data(aNew, bNew); // 创建新数据
                // CAS替换整个对象
            } while (!dataRef.compareAndSet(oldData, new Data(aNew, bNew)));
        }
    }

总结

  1. 三个核心参数

    • 内存地址 V:要操作的变量在内存中的地址(如 AtomicInteger 中 value 字段的偏移量 valueOffset)。
    • 预期值 A:线程认为变量当前应该的值(如 AtomicInteger 中 volatile 修饰的 value 的当前值)。
    • 新值 B:线程希望将变量更新为的值(如 value + 1)。
  2. 操作逻辑

    • 原子性地比较内存地址 V 中的实际值与预期值 A:
      • 若相等,将该值更新为 B,返回 true(操作成功)。
      • 若不相等,不做任何操作,返回 false(操作失败)。
    • 失败时通常通过自旋重试(循环重新获取最新值并再次尝试),直到成功。
  3. Java 中的实现

    • 底层依赖 sun.misc.Unsafe 类的 native 方法(如 compareAndSwapInt),直接调用 CPU 原子指令(如 x86 的 cmpxchg)。
    • 典型应用:java.util.concurrent.atomic 包下的原子类(如 AtomicInteger),通过 CAS 实现线程安全的自增、更新等操作。
  4. 主要缺点
    1. ABA 问题:

    现象:变量值从 A 被修改为 B 后又改回 A,CAS 会误认为值未变而成功执行更新,导致数据 不一致(如链表节点的重复引用问题)。
    解决方案:使用 AtomicStampedReference,通过引入版本号(或时间戳),更新时同时验证值和版本号,确保操作的安全性。
    2. 自旋开销大:
    现象:CAS 依赖自旋重试机制,若并发冲突频繁(如高并发场景下大量线程竞争同一变 量),会导致线程长时间循环重试,持续占用 CPU 资源,降低系统性能。
    优化思路: 限制自旋次数(如 JUC 中某些类的实现会在重试一定次数后阻塞)。 使用分段锁(如 LongAdder 将变量分段,降低冲突概率)。
    3. 仅支持单个变量的原子操作:
    现象:CAS 只能保证对单个共享变量的操作原子性,无法直接处理多个变量的组合原子操作 (如同时更新 a 和 b,要求两者要么都成功,要么都失败)。
    解决方案: 用 AtomicReference 将多个变量封装为一个对象,通过更新对象实现组合操作的原子性。 在复杂场景下改用 synchronized 或 ReentrantLock 等锁机制。

更多推荐