上一篇讲锁升级时,轻量级锁那一级反复出现一个词------CAS 。线程抢轻量级锁靠它、自旋重试也靠它。这一篇就把这个藏在幕后的主角请到台前:CAS 到底是什么?它凭什么能在"不加锁"的情况下,保证 count++ 这种复合操作的原子性? 要知道,第 3 篇我们才刚说 volatile 做不到原子的自增、得上 synchronized------可整个 java.util.concurrent 包里,有一大批类偏偏做到了"无锁 + 原子",它们靠的正是 CAS。
CAS 是理解 Java 并发的一道分水岭 。它上承 synchronized(互斥的悲观思路),下启 AtomicInteger、ConcurrentHashMap、AQS(无锁/乐观的思路)------可以说,不懂 CAS,后面 AQS、并发容器就都是空中楼阁 。面试里 CAS 也是必考,而且考得深:不光问"是什么",还会追着问 ABA、问自旋开销、问 LongAdder 为什么比 AtomicLong 快。这一篇就沿着这条追问链,把 CAS 从原理到坑、再到它的进化形态一次讲清。
这篇按这条线索展开:先讲清 CAS 这条"硬件指令级的乐观主义"到底怎么工作、它和锁的思路差在哪;再拆开 AtomicInteger,看它如何用"自旋 CAS"实现无锁自增;然后正视 CAS 的三个软肋(ABA、自旋开销、只能保一个变量)以及各自的解法;接着俯瞰整个 Atomic 家族;最后落到高并发下的进化版 LongAdder,看它怎么用"分而治之"把 CAS 的性能再压榨一截。
目录
- CAS:一条硬件指令的乐观主义
- AtomicInteger:无锁自增怎么做到的
- [CAS 的三个软肋](#CAS 的三个软肋)
- [Atomic 家族全景](#Atomic 家族全景)
- LongAdder:把热点打散
- 小结:无锁世界的地基
一、CAS:一条硬件指令的乐观主义
CAS 是 Compare-And-Swap(比较并交换) 的缩写。它是一个原子操作,接收三个操作数:
- V:要修改的内存位置(变量的当前值);
- A:预期的旧值(expected);
- B:想写入的新值(new)。
它干的事一句话说清:"如果 V 现在的值 == A,就把它更新成 B;否则什么都不做。" 而这个"比较 + 交换"整体是原子的,中间不会被打断。 无论成功失败,它都会返回 V 原本的值(或一个 boolean 表示成没成)。
关键在于为什么它是原子的 。CAS 不是 Java 层面用什么巧妙代码拼出来的,而是直接对应一条 CPU 指令 (x86 上是 cmpxchg,加 lock 前缀保证多核下的原子性)。"比较"和"交换"这两步在硬件层面就是一体的、不可分割的------正因为落到了硬件那一层,它才能绕开操作系统的锁,天然原子。这是理解 CAS 的第一块基石:它的原子性来自硬件,不是来自锁。
理解 CAS,最好的方式是把它和 synchronized 的思路对照------这是悲观锁 vs 乐观锁的经典分野:
synchronized是悲观锁 :它悲观地假设"我改这个数据时,一定有人会来抢",所以先把锁拿到手、把别人挡在门外,再安心修改。代价是别人得阻塞等待。- CAS 是乐观锁 :它乐观地假设"我改的时候,多半没人来抢",所以不加锁直接改 ,只在真正写入的那一刻用 CAS 检查一下"我读到值之后、到现在为止,有没有人动过它"。没人动过(V 还等于 A),改成功;有人动过(V 变了),说明这次冲突了,那就放弃重来,一切照旧、不阻塞任何人。
这个对照很关键:悲观锁把成本花在"事前防范"(加锁阻塞),乐观锁把成本花在"事后重试"(CAS 失败再来)。 所以它们的适用场景天然不同------竞争激烈时,悲观锁更好 (重试太多次不如老实排队);竞争不激烈时,乐观锁完胜 (大概率一次成功,省掉了加锁、阻塞、上下文切换的全部开销)。这也正好解释了上一篇锁升级的逻辑:轻度竞争用 CAS(轻量级锁),竞争激烈了才升级去阻塞(重量级锁)------锁升级本质上就是"乐观打不过了,退回悲观"。
二、AtomicInteger:无锁自增怎么做到的
理论说完,看它怎么解决第 3 篇那个悬案------不加锁地实现原子自增 。java.util.concurrent.atomic.AtomicInteger 就是答案:
java
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 原子地 +1,线程安全,且无锁
它内部怎么做到的?看 incrementAndGet 的核心逻辑(简化版,本质就是"自旋 CAS"):
java
public final int incrementAndGet() {
int prev, next;
do {
prev = get(); // ① 读当前值(比如读到 100)
next = prev + 1; // ② 算出新值(101)
} while (!compareAndSet(prev, next)); // ③ CAS:若当前还是 prev 就写入 next,否则重来
return next;
}
把这三步和第 3 篇那个出 bug 的 count++ 对照,精髓就出来了:
- 普通
count++的三步(读、改、写)之间门户大开,别人能插进来,于是丢更新; AtomicInteger也是读、改、写三步,但它把"写"换成了 CAS ------写入前会验货 :"我读的时候是 100,现在还是 100 吗?" 如果是,说明这期间没人动过,安全写入;如果不是(被别人改成了别的值),CAS 失败,while循环自旋重试:重新读最新值、重新计算、再 CAS,直到成功为止。
这个"读 → 改 → CAS,失败就转圈重来 "的循环,就是自旋 CAS,是所有原子类的核心套路:

再往底层扒一层:compareAndSet 最终调用的是 sun.misc.Unsafe 类的本地方法 compareAndSwapInt。Unsafe 是 JDK 内部一个"能直接操作内存"的后门类 ------它能拿到对象字段的内存偏移量、直接对那块内存做硬件级 CAS。整个 java.util.concurrent 的无锁能力,几乎都建立在 Unsafe 提供的 CAS 之上。(Unsafe 名副其实地"不安全",绕过了 JVM 的安全检查,所以官方不让应用代码直接用,但了解它的存在,能帮你看懂原子类的底裤。)
JDK 9 起的小变化 :从 JDK 9 开始,官方提供了
VarHandle作为Unsafe那些 CAS 操作的正式、安全的替代 ,原子类内部实现也逐步迁了过去。原理完全一样(还是硬件 CAS),只是换了个不那么"危险"的门面。本系列主线 JDK 8 里用的还是Unsafe,知道 JDK 9+ 有VarHandle这回事即可。
三、CAS 的三个软肋
CAS 很美,但不是银弹。它有三个必须知道的软肋,也是面试的高频追问点。
软肋一:ABA 问题------"值没变"不等于"没被动过"。
CAS 判断的依据是"值等不等于预期",但这里藏着一个漏洞:如果一个值从 A 被改成了 B、又被改回了 A,CAS 会以为"它没变过",照样交换成功------可实际上它已经被人动过一轮了。 这就是 ABA 问题。
打个比方:你出门前看杯子里有半杯水(A),回来一看还是半杯水(A),就以为没人碰过。但其实中间有人把水喝光了(B)、又给你续了半杯(A)。对纯数值的自增来说,ABA 通常无害(100 变成 100 就是 100);但在涉及引用、涉及"过程"的场景(比如无锁栈的出栈入栈、对象被回收又重新分配到同一地址)里,ABA 会造成真实的逻辑错误。
解法是给值加一个"版本号/时间戳" ,让每次修改都让版本号 +1------这样即便值转了一圈回到 A,版本号也变了,CAS 就能识破。Java 提供了现成的 AtomicStampedReference (带 int 版本戳)和 AtomicMarkableReference(带一个 boolean 标记,只关心"动没动过"):
java
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0); // 值100,版本0
// CAS 时要同时比对"值"和"版本号",两者都对才成功
ref.compareAndSet(100, 101, 0, 1); // 期望值100+期望版本0 → 新值101+新版本1
软肋二:自旋开销------竞争激烈时,转圈也很费。
自旋 CAS 的前提是"大概率一次成功"。可一旦竞争非常激烈 ,大量线程会一遍遍 CAS 失败、一遍遍空转重试,白白烧 CPU 却推进不了几个。这时候,自旋的总开销可能反而超过直接用锁阻塞。所以 CAS 更适合"竞争不激烈、临界区操作简单 "的场景------这也再次印证了乐观锁的适用边界。(LongAdder 就是专门来救这个场的,下一节讲。)
软肋三:只能保证一个变量的原子性。
CAS 一次只能对一个 内存位置做原子操作。如果你需要"同时 "原子地更新多个变量(比如转账要同时改两个账户余额),CAS 就无能为力了------它保不了"多个变量作为一个整体"的原子性。这种场景要么老实用锁,要么把多个变量封装进一个对象 ,然后用 AtomicReference 对这个对象的引用做 CAS(把"多变量"变成"单引用")。
把三个软肋记牢,你对 CAS 的认知才算完整:它是乐观锁,怕 ABA、怕激烈竞争、只能管一个变量。 用对地方是神器,用错地方是灾难。
四、Atomic 家族全景
java.util.concurrent.atomic 包里是一整个家族,全都基于 CAS,按用途分成几类。不用死记,理解每类解决什么问题即可:
| 类别 | 代表类 | 用途 |
|---|---|---|
| 基本类型 | AtomicInteger、AtomicLong、AtomicBoolean |
对单个基本类型变量做原子操作 |
| 引用类型 | AtomicReference |
对一个对象引用做原子 CAS(把"多变量"打包成"单引用"的手段) |
| 带版本引用 | AtomicStampedReference、AtomicMarkableReference |
解决 ABA 问题(上一节) |
| 数组 | AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray |
对数组里某个元素做原子操作 |
| 字段更新器 | AtomicIntegerFieldUpdater 等 |
把某个类里已有的 volatile 字段"升级"成能原子更新,省去改成 Atomic 类型 |
| 累加器(JDK 8+) | LongAdder、LongAccumulator、DoubleAdder |
高并发计数场景,性能优于 AtomicLong |
几个实用提醒:
AtomicBoolean常用来做"只执行一次"的开关(比如保证某个初始化只跑一次):if (inited.compareAndSet(false, true)) { init(); }。AtomicReference是解决"CAS 只能保一个变量"的标准手段------把要一起改的字段塞进一个不可变对象,然后 CAS 整个引用。- 字段更新器 用得少,但在"不想把字段类型从
volatile long改成AtomicLong、又想偶尔原子更新"时能省内存(每个AtomicLong对象都有额外开销,字段更新器是共享的静态工具)。
家族虽大,地基只有一个------CAS。下一节的 LongAdder 是这个家族里最值得单独讲的一个,因为它代表了 CAS 的一次重要进化。
五、LongAdder:把热点打散
AtomicLong 在高并发计数下有个天生的瓶颈,正是第三节的"软肋二":所有线程都盯着同一个变量做 CAS,竞争一激烈,绝大多数线程都在自旋失败、反复重试 ,CPU 空转严重。设想一个高 QPS 接口用 AtomicLong 做全局计数器,几百个线程抢着 CAS 同一个 value------这就成了性能热点。
LongAdder(JDK 8 引入)的解法非常漂亮,核心思想是分而治之 / 空间换时间:
既然大家抢一个变量抢得凶,那就别让大家抢同一个------把这个计数"拆散"成多个,让不同线程去改不同的那份,各改各的,最后要总数时再把它们加起来。
具体来说,LongAdder 内部维护一个 base 值和一个 Cell[] 数组(每个 Cell 是一个独立的计数单元):
- 竞争不激烈时 :直接 CAS 更新
base,和AtomicLong一样简单; - 竞争激烈(CAS
base失败)时 :不再死磕base,而是根据线程去散列(hash)到Cell[]数组里的某个格子 ,去 CAS 那个格子。不同线程被分散到不同 Cell 上,把"一个热点"打散成"多个冷点",CAS 冲突概率骤降; - 求总数时 :
sum() = base + 所有 Cell 之和。

这个"分段"的思路,和 ConcurrentHashMap 1.7 的分段锁、和数据库的分库分表,本质是同一种智慧:降低单点竞争,就把单点拆成多点。 记住这个模式,后面会反复见到它。
不过 LongAdder 有一个必须知道的代价 ------它的 sum() 在并发环境下不是精确的强一致值 :因为求和时它逐个读取 base 和各个 Cell,而这期间别的线程可能还在修改某些 Cell,所以 sum() 拿到的是一个"最终一致"的近似快照。这带来一个清晰的选型标准:
- 需要高并发下频繁计数、且能容忍统计值有瞬时误差 (如监控埋点、访问量统计、限流计数)→ 用
LongAdder,吞吐高得多; - 需要每一步都拿到精确、强一致的当前值 (如严格的库存扣减,读了就要立刻据此决策)→ 老实用
AtomicLong(或加锁),别为了性能牺牲正确性。
一句话:LongAdder 用"求和不精确"换"更新更快",是典型的场景权衡------高频写、低频读且容忍误差,它就是最优解。
六、小结:无锁世界的地基
这一篇我们把并发编程的另一条大动脉------无锁(lock-free) ------的源头讲清楚了。它和上一篇的 synchronized 是并发的两种世界观:一个悲观地加锁挡人,一个乐观地直接改、冲突了再重试。
回顾这条链子:CAS 是硬件提供的原子"比较并交换"指令 → 乐观锁的思路,用它把"读-改-写"的写入变成"验货后再写、失败就自旋重来" → AtomicInteger 等原子类由此实现无锁的线程安全 → 但 CAS 有 ABA、自旋开销、单变量三个软肋 → LongAdder 用分段把高并发下的自旋热点打散。 更重要的是,CAS 是后面 AQS (第 5 篇)、ConcurrentHashMap(第 9 篇)的共同地基------它们的"无锁"底气全来自这里。
带走三句话:① CAS 是乐观锁,原子性来自硬件指令,不是来自锁;② 它的三个软肋要张口就来------ABA(用带版本号的 AtomicStampedReference 解)、激烈竞争下自旋烧 CPU、只能保一个变量;③ 高并发计数用 LongAdder(分段打散热点),但它 sum() 不精确,要精确值仍用 AtomicLong。
下一篇,我们去啃并发包里最硬的一块骨头------AQS(AbstractQueuedSynchronizer) 。ReentrantLock、CountDownLatch、Semaphore、线程池......半个 java.util.concurrent 都建在它身上。而它的骨架,正是"一个 volatile 的 state + 一个 CAS + 一个等待队列"------你会发现,这一篇的 CAS,就是打开那扇门的钥匙。