Skip to content

CAS 与无锁原子基础 ​

一句话定义 ​

CAS(Compare-And-Swap) 是 CPU 提供的原子原语:「只有当内存里的值仍是我看到的旧值时,才写成新值」;是 Atomic*、很多无锁结构的基石,属于乐观并发思路。

可运行对比实验:volatile-vs-atomic-vs-lock · VolatileVsAtomicVsLock.java


为什么要学 ​

读 volatile、AtomicInteger、ConcurrentHashMap 之前,需要分清:

概念解决什么
volatile可见性 + 部分有序性;不保证 count++
CAS / Atomic*单变量的原子读-改-写
synchronized / Lock互斥 + 多步复合逻辑

CAS 在做什么(口语) ​

比喻:白板上的数字是 10。你和小王都要改成 11:

  1. 你先读:旧值 = 10
  2. 你算:新值 = 11
  3. 你去换:仅当白板上仍是 10 才写成 11;若已被别人改成 11,你失败重试

这就是 compareAndSet(expected, update) 的心智模型。

java
// AtomicInteger 内部语义(简化)
boolean compareAndSet(int expect, int update) {
    // 硬件保证:读-比较-写 这一步原子
    if (current == expect) { current = update; return true; }
    return false;
}

CPU 层到底做了什么(为什么"读-比较-写"能原子) ​

普通的三步指令(读 → 比较 → 写)在 CPU 上会被其他核的写入从中间打断;CAS 之所以原子,靠的是硬件指令,而不是 Java 代码:

  • x86:LOCK CMPXCHG —— LOCK 前缀让指令执行期间锁住缓存行(Cache Line)/ 总线,其他核的读写被阻塞到指令完成。
  • ARM:LDXR / STXR(Load-Exclusive / Store-Exclusive)—— 读标记独占,写时若发现该地址已被其他核改过则失败返回,由软件重试。
  • 依赖的底层机制是 缓存一致性协议(MESI) :保证一个核写回后,其他核能观察到最新值,LOCK 再把"检查 + 写入"合并成一个不可分割的步骤。
text
线程 A 执行 compareAndSet(10, 11)
   │
   ├─ 1. 读内存: current == 10 ?            ┐
   ├─ 2. 比较: 10 == 10 ? 是                │  LOCK CMPXCHG 期间
   ├─ 3. 写入: 内存 ← 11  ✅                │  总线/缓存行被锁定,
   └─ 4. 返回 true                           │  线程 B 的写入必须等待

自旋重试(Spin Retry):失败不是异常,重试是常态 ​

CAS 失败只表示"有人抢先改了值",正确的姿势是读新值 → 再算 → 再 CAS,直到成功:

java
// 手写自旋版:等价于 incrementAndGet() 内部行为
AtomicInteger counter = new AtomicInteger(0);

int increment() {
    int prev, next;
    do {
        prev = counter.get();          // 1. 读当前值
        next = prev + 1;               // 2. 计算新值
    } while (!counter.compareAndSet(prev, next)); // 3. 失败就带着新 prev 重试
    return next;
}
  • 可能执行顺序:线程 T1 先 compareAndSet(0, 1) 成功;线程 T2 同时带着旧值 0 重试 → 比较失败 → 重新读得 1 → 再 CAS (1, 2) 成功。
  • 预期现象:所有并发线程最终都把计数 +1,且不会丢更新(这正是 count++ 做不到的)。
  • 观察重点:高冲突时循环可能空转多次(自旋烧 CPU),所以 LongAdder 用分段 Cell 分散写热点来减少冲突概率。

优劣 ​

优势劣势
无锁(通常不阻塞线程)高冲突时自旋重试,烧 CPU
低冲突下延迟小只适合单变量或能封装成原子状态的问题
表达「乐观更新」清晰ABA 等语义问题要额外处理
适合计数、状态位、引用切换多字段一致性仍要锁或重新设计

乐观锁 vs 悲观锁(概念层) ​

乐观(CAS / Atomic*)悲观(synchronized / Lock)
心态先假设不会冲突,冲突再重试先占坑再干活
Java 代表AtomicInteger、LongAddersynchronized、ReentrantLock
冲突低通常更快略重
冲突高自旋可能更差排队阻塞可能更稳

本文只建立概念;代码对比见 volatile lab 场景 2。


Java 里常见 API ​

API用途
AtomicInteger / AtomicLong计数、序号
AtomicReference<V>引用整体切换(配置快照)
AtomicStampedReference带版本号,缓解 ABA
LongAdder极高并发计数,空间换冲突分散
VarHandle(JDK 9+)更底层的 CAS 入口

incrementAndGet() 等在底层就是 CAS 循环。


ABA 问题(知道即可) ​

线程 A 读到值 V1 → 被 B 改成 V2 又改回 V1 → A 的 CAS 仍成功,但中间状态已变。
引用类型若不能容忍「对象已被换掉又换回来」,用 AtomicStampedReference 或加版本号。


Android / 工程常见用途 ​

场景倾向
点赞数、下载计数AtomicInteger / LongAdder
一次性开关、熔断AtomicBoolean
配置引用整体替换AtomicReference + 不可变对象
UI 状态多字段联动不用 Atomic 硬凑,用不可变 state + StateFlow 或锁
ConcurrentHashMap 桶头 CAS集合底层;见 map lab

和后续专题的关系 ​

图注补充:volatile-vs-atomic-vs-lock lab。


复习检查题 ​

  1. CAS 保证的是什么?不保证什么?

    答:保证单次「读旧值 → 比较 → 条件成立才写入」在硬件上是原子的,适合单变量读-改-写(如 incrementAndGet)。不保证多字段一致性、长临界区业务逻辑,也不自动解决 ABA;高冲突时只保证正确性,不保证性能(会自旋烧 CPU)。

  2. 为什么 volatile 不能替代 AtomicInteger 做 count++?

    答:count++ 是读、加、写三步,volatile 只保证每次读/写可见,不保证三步整体互斥。多线程仍会丢更新。AtomicInteger 用 CAS 把「比较旧值再写入」合成一步原子语义。

  3. 高并发计数何时考虑 LongAdder 而不是 AtomicLong?

    答:写冲突极高、热点集中在少数计数器时,AtomicLong 会大量 CAS 重试。LongAdder 用分段 Cell 分散写压力,读时求和,以空间换吞吐。读多写少、需要强一致实时读值时仍可能用 AtomicLong 或锁。

  4. 什么是 ABA?什么场景需要关心?

    答:线程 A 读到值 V1,期间被改成 V2 又改回 V1,A 的 CAS 仍成功,但中间状态已变。纯数值计数通常无所谓;引用/链表节点等语义下,「对象已被替换又换回来」可能出错,需 AtomicStampedReference 或版本号。

速记 ​

  • CAS = 比较旧值再更新,失败就重试。
  • Atomic* = 乐观、单变量;锁 = 悲观、复合逻辑。
  • 冲突低用 CAS 爽,冲突高要警惕自旋。

站点构建时间:2026/8/24 23:43:17