CAS 与原子类

举报
yd_287908631 发表于 2026/10/29 09:22:43 2026/10/29
【摘要】 从 cmpxchg 讲到原子类与 LongAdder 的取舍。

volatile 解决了可见性,却解决不了 count++ 的原子性。加锁可以,但阻塞和唤醒有成本。CAS(Compare-And-Swap)提供了一条无锁路径:用一条 CPU 指令完成"比较并交换",失败就重试。java.util.concurrent.atomic 包下的所有原子类都建立在这个原语之上。本文从硬件指令讲到 AtomicInteger 的源码,再说明高竞争下为什么 LongAdder 更合适。

一、CAS 的语义与硬件支持

CAS 包含三个操作数:内存位置 V、期望值 A、新值 B。仅当 V 的当前值等于 A 时,才把 V 更新为 B,否则不做任何事并返回失败。整个过程是一条原子指令。

x86 上对应 cmpxchg 指令,多核环境下会加上 lock 前缀,保证缓存一致性:

lock cmpxchg [addr], new_value

lock 前缀会锁住缓存行(或总线),使其他核心的缓存副本失效,从而保证读-比较-写三步不可分割。这也是 CAS 在极高竞争下并不便宜的原因:缓存行会在核心间来回弹跳。

Java 通过 sun.misc.Unsafe(JDK 9 起逐步迁移到 VarHandle)暴露这套能力:

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

offset 是字段在对象中的内存偏移量,通过 objectFieldOffset 获取。

二、AtomicInteger 的实现

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final Unsafe U = Unsafe.getUnsafe();
    private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");

    private volatile int value;   // volatile 保证可见性

    public final int get() {
        return value;
    }

    public final int incrementAndGet() {
        return U.getAndAddInt(this, VALUE, 1) + 1;
    }

    public final boolean compareAndSet(int expectedValue, int newValue) {
        return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
    }
}

incrementAndGet 的底层是一个自旋循环(getAndAddInt):

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);       // 读当前值
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));  // CAS 失败就重试
    return v;
}

value 必须声明为 volatile:CAS 只保证"比较并交换"这一步原子,读取当前值这一步需要 volatile 保证读到的是主内存最新值,否则自旋可能一直基于过期值失败。

JDK 9 之后,AtomicInteger 内部改用 VarHandle,但语义完全一致,只是把 Unsafe 的裸偏移量封装得更安全。

AtomicInteger 还提供了 lazySet,语义上是"最终可见":

public final void lazySet(int newValue) {
    U.putIntRelease(this, VALUE, newValue);   // release 语义,不保证立即刷主存
}

它比 set 便宜,因为不需要 StoreLoad 全屏障,适合"我改了,别人晚一点看到没关系"的场景,比如更新一个统计标记。但用它做锁释放或状态切换会出问题,因为后继的读可能看不到这次写。这个 API 名字里的 "lazy" 容易让人误以为只是"延迟执行",实际上它是一个弱化版的 volatile 写。

三、ABA 问题

CAS 只比较值是否相等,无法感知"值变过又变回来"。这就是 ABA 问题:

线程 1:读取 value = A
线程 2:把 A 改成 B,再改回 A
线程 1:CAS(A, C) 成功

线程 1 以为没人动过,实际上中间已经发生了变化。对单纯的计数器无所谓,但对"值代表状态或引用"的场景可能是致命的。典型例子是栈的 pop:

// 线程 1 准备 pop,读到栈顶是 A,next 是 B
// 线程 2 pop 了 A 和 B,又 push 了 A
// 线程 1 CAS 把栈顶从 A 换成 B,但 B 已经被弹出,栈结构损坏

JDK 提供了 AtomicStampedReference,给值附加一个版本号:

AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

int stamp = ref.getStamp();
String value = ref.getReference();
// 只有值和版本号都匹配才更新
boolean ok = ref.compareAndSet(value, "C", stamp, stamp + 1);

另一个选择是 AtomicMarkableReference,用一个 boolean 标记代替版本号,适合"是否被删除/标记"这类二值状态。

四、常用原子类一览

| 类 | 作用 | 备注 |

| --- | --- | --- |

| AtomicInteger / AtomicLong | 整型原子操作 | 最常用 |

| AtomicBoolean | 布尔原子操作 | 一次性开关 |

| AtomicReference<T> | 对象引用原子更新 | 配合 updateAndGet 做函数式更新 |

| AtomicIntegerArray / AtomicLongArray | 数组元素原子操作 | 元素级可见性 |

| AtomicReferenceArray<E> | 对象数组原子操作 | |

| AtomicStampedReference | 带版本号的引用 | 解决 ABA |

| AtomicMarkableReference | 带标记的引用 | 二值状态 |

| LongAdder / LongAccumulator | 分片累加 | 高并发计数 |

| DoubleAdder | 浮点分片累加 | |

AtomicReference 的 updateAndGet 在 JDK 8 中是这样实现的:

public final V updateAndGet(UnaryOperator<V> updateFunction) {
    V prev, next;
    do {
        prev = get();
        next = updateFunction.apply(prev);   // 注意:函数可能被多次执行
    } while (!compareAndSet(prev, next));
    return next;
}

updateFunction 必须是无副作用的纯函数,因为 CAS 失败时它会被重复调用。如果函数里有计数、日志、远程调用,就会执行多次。

五、LongAdder:高竞争下的替代方案

AtomicLong 的所有线程都在 CAS 同一个 value,竞争激烈时大量自旋失败,吞吐量骤降。LongAdder 采用分片思想:

LongAdder
├── base = 10
└── Cell[] cells
      ├── Cell[0] = 3
      ├── Cell[1] = 5
      ├── Cell[2] = 0
      └── Cell[3] = 7
     sum() = base + 3 + 5 + 0 + 7 = 25

每个线程根据自己的 probe 值散列到不同的 Cell 上累加,只有在散列冲突时才 CAS 同一个槽。sum() 时把 base 和所有 Cell 加起来。

public void add(long x) {
    Cell[] cs; long b, v; int m; Cell c;
    if ((cs = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        if (cs == null || (m = cs.length - 1) < 0 ||
            (c = cs[getProbe() & m]) == null ||
            !(uncontended = c.cas(v = c.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}

取舍:

| 维度 | AtomicLong | LongAdder |

| --- | --- | --- |

| 低竞争 | 快,单次 CAS | 略慢,有分片判断开销 |

| 高竞争 | 大量自旋,吞吐下降 | 接近线性扩展 |

| 读取 | O(1) 精确值 | O(n) 汇总,且不精确 |

| 内存 | 一个变量 | 每个 Cell 一个缓存行(通常填充到 64 字节) |

sum() 在并发累加过程中返回的是近似值,不能用于需要强一致的场景(如库存扣减的余额判断)。适合统计 QPS、请求计数这类"最终准确即可"的指标。

LongAdder 的 sum() 不是原子的,遍历 cells 时如果有线程正在累加,可能读到某个 Cell 的旧值。sumThenReset() 同样不是原子的,并发调用会丢失部分计数。需要"读取并清零"的原子语义时,用 AtomicLong.getAndSet(0),或在外层加锁。

LongAccumulator 是更通用的版本,可以自定义累加函数:

LongAccumulator max = new LongAccumulator(Math::max, Long.MIN_VALUE);
max.accumulate(10);
max.accumulate(3);
System.out.println(max.get());   // 10

它要求累加函数满足交换律和结合律,因为分片累加的顺序不确定。减法、除法这类不满足交换律的运算不能直接用作累加函数。

六、可运行的对比示例

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

public class CounterBenchmark {
    private static final int THREADS = 16;
    private static final int LOOPS = 1_000_000;

    public static void main(String[] args) throws Exception {
        run("AtomicLong", () -> {
            AtomicLong c = new AtomicLong();
            runThreads(c::incrementAndGet);
            return c.get();
        });

        run("LongAdder", () -> {
            LongAdder c = new LongAdder();
            runThreads(c::increment);
            return c.sum();
        });

        run("synchronized", () -> {
            long[] c = {0};
            runThreads(() -> { synchronized (c) { c[0]++; } });
            return c[0];
        });
    }

    private static void runThreads(Runnable task) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(THREADS);
        CountDownLatch start = new CountDownLatch(1);
        CountDownLatch done = new CountDownLatch(THREADS);
        for (int i = 0; i < THREADS; i++) {
            pool.execute(() -> {
                try {
                    start.await();
                    for (int j = 0; j < LOOPS; j++) task.run();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    done.countDown();
                }
            });
        }
        start.countDown();
        done.await();
        pool.shutdown();
    }

    private static void run(String name, Callable<Long> action) throws Exception {
        long t0 = System.nanoTime();
        long value = action.call();
        long ms = (System.nanoTime() - t0) / 1_000_000;
        System.out.printf("%-14s value=%-12d %d ms%n", name, value, ms);
    }
}

在 16 线程、1600 万次自增下,LongAdder 通常明显快于 AtomicLong,而 synchronized 最慢。具体数字取决于机器,但趋势稳定。这个例子同时验证了结果正确性:三者最终值都等于 THREADS * LOOPS。

七、并发场景分析

自旋的 CPU 代价。CAS 失败会立即重试,竞争激烈时多个线程空转烧 CPU。这也是 AtomicLong 在高并发下吞吐反而下降的原因。缓解手段是分片(LongAdder)或退化为锁。

只保证单个变量的原子性。多个原子变量之间没有整体原子性。下面这段代码不是原子的:

AtomicInteger x = new AtomicInteger();
AtomicInteger y = new AtomicInteger();

x.incrementAndGet();   // 两个操作之间可能被其他线程插入
y.incrementAndGet();

需要多个变量一致时,用 AtomicReference 包一个不可变对象,或直接加锁。

CAS 与对象发布。用 AtomicReference 做无锁栈或队列时,节点的 next 必须在发布前设置完成。AtomicReference 的 CAS 本身提供 volatile 语义,能保证之前的写入对读到该引用的线程可见,这是无锁数据结构正确性的基础。

与 volatile 的关系。原子类的 get/set 具备 volatile 读写语义,compareAndSet 还额外保证原子性。所以用原子类时不需要再给字段加 volatile。

八、常见坑 / 最佳实践

  1. updateAndGet 的函数必须无副作用。CAS 失败会重试,函数可能执行多次。
  2. 注意 ABA。值代表状态或引用时用 AtomicStampedReference,纯计数场景不必。
  3. 高并发计数优先 LongAdder,但读取是近似值,不能用于强一致判断。
  4. 原子类不保证多变量原子性。多个字段需要一致时用 AtomicReference 包不可变对象,或加锁。
  5. 不要用原子类替代所有锁。临界区包含多个操作或需要条件等待时,锁更清晰也更安全。
  6. AtomicInteger 不能当锁对象。虽然能用,但包装类缓存和身份语义容易引发误用,锁对象用专用 Object。
  7. JDK 9+ 可用 VarHandle 替代 Unsafe,API 更安全,但原理相同。

九、小结

  • CAS 是 lock cmpxchg 提供的一条原子指令,Java 通过 Unsafe/VarHandle 调用。
  • AtomicInteger 用 volatile value 加 CAS 自旋实现,value 的 volatile 不可省略。
  • ABA 问题用 AtomicStampedReference 加版本号解决,纯计数场景通常不需要。
  • LongAdder 用 base 加 Cell[] 分片降低竞争,适合高并发统计,但 sum() 是近似值。
  • 原子类只保证单变量原子性,多变量一致性仍需 AtomicReference 包不可变对象或加锁。
  • updateAndGet 的更新函数会被重复执行,必须无副作用。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。