CAS 适合对单个状态执行“值仍是我看到的旧值时才更新”。它避免线程阻塞,但不是没有成本:高竞争会造成反复失败、 缓存一致性流量和不公平。多字段业务不变量通常仍需要锁或不可变状态整体替换。
Table of contents
运行 ABA 实验
下载带版本戳的 ABA 样例下载, 运行:
java -cp target/classes dev.elaine.concurrency.AtomicAbaSample
输出类似:
值=A,版本=2,旧观察能否更新=false
线程最初观察到 A@0。另一个执行流把它改为 B@1,再改回 A@2。只比较值时前后都是 A,这就是 ABA 问题;
同时比较版本戳后,旧观察无法覆盖新历史。
CAS 做了什么
**CAS(Compare-And-Set)**接收预期旧值和新值。内存中的值等于预期值时原子写入新值并返回成功,否则不修改并返回 失败。
var counter = new java.util.concurrent.atomic.AtomicInteger();
int next = counter.incrementAndGet();
AtomicInteger 把重试循环封装起来,并提供相应内存语义。不要用 volatile int 加 ++ 替代。
失败重试会消耗资源
典型 CAS 循环如下:
for (;;) {
int current = counter.get();
int next = current + 1;
if (counter.compareAndSet(current, next)) return next;
Thread.onSpinWait();
}
高竞争时,许多线程可能同时读取同一旧值,只有一个成功,其余继续占用 CPU。需要退避、分片或更高层结构。统计热点
可以使用 LongAdder 分散竞争,但 sum() 不是与所有并发更新形成单一原子快照,不适合余额等强一致值。
ABA 何时有害
只有历史变化影响正确性时,ABA 才是问题。无锁栈中节点 A 被弹出、复用后又放回,旧 CAS 可能误以为链表从未改变。 解决方案包括版本戳、不可复用标识、不可变节点,或使用已经验证的 JDK 并发容器。
AtomicStampedReference 同时比较引用和整数版本;AtomicMarkableReference 只携带布尔标记。不要为了“无锁”自行实现复杂
内存回收算法。
下一步
阅读AQS 与 ReentrantLock 源码主线,理解 CAS 失败后线程如何进入 等待队列并被唤醒。