从一个计数器说起
java
public class Counter {
private int count = 0;
public void increment() {
count++; // 线程不安全
}
}
多线程下 count++ 不是原子操作,导致计数偏少。你可能会说:加 synchronized 就好了。但有没有不加锁也能保证线程安全的方式?
有------原子类 。AtomicInteger 的 incrementAndGet() 可以在不加 synchronized 的情况下实现线程安全的自增。它背后的核心就是 CAS(Compare And Swap)。
面试中,CAS 是必考点。但很多人只知道 CAS 是乐观锁,却回答不上来:
- CAS 的底层 CPU 指令是什么?
- ABA 问题是怎么产生的,如何解决?
- LongAdder 为什么比 AtomicLong 更快?
- Unsafe 类为什么叫 Unsafe?
本文从底层硬件到上层 API,彻底拆解 Java 原子类和 CAS 的实现原理。
本文核心问题:
- CAS 是什么?它为什么能不加锁实现原子操作?
- CAS 的底层 CPU 指令是什么?Unsafe 类扮演了什么角色?
- ABA 问题是怎么产生的?如何用版本号解决?
- AtomicInteger 的 incrementAndGet() 源码是怎么写的?自旋重试多少次?
- LongAdder 为什么比 AtomicLong 在高并发下更快?Cell 是怎么分散压力的?
- AtomicReference、AtomicStampedReference、AtomicMarkableReference 各自适用什么场景?
- CAS 有什么缺点?什么情况下不适合用 CAS?
- 原子类在 JDK 中有哪些应用?线程池、AQS、ConcurrentHashMap 中分别怎么用?
读完本文你将对 CAS 和原子类有从硬件指令到框架应用的完整理解。
一、CAS 是什么?------一条 CPU 指令的魔法
疑问:为什么 count++ 不能保证原子性,而 AtomicInteger.incrementAndGet() 可以?
回答:因为 count++ 在字节码层面是三步操作,而 incrementAndGet() 底层用了 CAS------一条 CPU 指令完成"比较并交换"。
1.1 count++ 的实际执行过程
java
count++; // 一行代码
编译成字节码后,实际上是三条指令:
1. getfield count // 从主内存读取 count 到操作数栈
2. iconst_1 // 常量1压入栈
3. iadd // 加法运算
4. putfield count // 写回主内存
多线程下的问题:线程 A 执行到第 3 步时,线程 B 可能已经写回了新值,线程 A 再写回就会覆盖线程 B 的结果。这就是经典的"读-改-写"竞态条件。
1.2 CAS 的思想
CAS(Compare And Swap):先比较,如果当前值等于我预期的值,就交换为新值;否则不交换。
CAS(内存地址 V, 预期值 A, 新值 B):
if (V.value == A) {
V.value = B;
return true;
} else {
return false;
}
关键 :这个"比较+交换"的过程是原子的,由 CPU 指令保证。
1.3 CAS 的底层实现------Unsafe 类 + CPU 指令
Java 中 CAS 操作的入口是 sun.misc.Unsafe 类:
java
public final class Unsafe {
// native 方法,直接调用 CPU 指令
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
}
底层 CPU 指令:
- x86/x64 架构:
lock cmpxchg(Compare and Exchange) - ARM 架构:
ldrex/strex(Load Exclusive / Store Exclusive)
lock 前缀的作用 :锁定总线或缓存行,保证多核 CPU 之间的原子性和可见性。执行 lock cmpxchg 时,其他 CPU 核心不能访问该内存地址。
二、ABA 问题------CAS 的阿喀琉斯之踵
疑问:CAS 看起来很完美,它有什么缺陷?什么是 ABA 问题?
回答:CAS 只比较值,不关心值的变化过程。如果一个值从 A 变成 B 又变回 A,CAS 发现还是 A,就会认为没有变化------这就是 ABA 问题。
2.1 ABA 问题的场景
初始值: A
线程 T1: 读取值为 A,准备 CAS 设为 C
线程 T2: 读取值为 A,CAS 设为 B(成功)
线程 T3: 读取值为 B,CAS 设回 A(成功)
线程 T1: CAS(预期A → 新值C) 成功! ← 但实际值已经经历过 A→B→A 的变化
单链表中的后果:
原链表: head → A → B → C → null
线程 T1: 准备 CAS 删除节点 A(将 head 指向 B)
线程 T2:
1. 删除 A(head 指向 B)
2. 删除 B(head 指向 C)
3. 重新插入 A(head 指向 A → C → null)
线程 T1: CAS 成功,head 指向 B
结果: head → B,但 B 已经被删除了!链表断裂!
2.2 解决方案:版本号
核心思想:除了比较值,还比较版本号。每次修改,版本号递增。
Java 提供了两个带版本号的原子类:
| 类 | 版本号机制 | 适用场景 |
|---|---|---|
AtomicStampedReference |
整型版本号(stamp) | 需要精确跟踪修改次数 |
AtomicMarkableReference |
布尔标记位 | 只关心"是否被修改过" |
java
// AtomicStampedReference 示例
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
int[] stamp = new int[1];
String value = ref.get(stamp); // value = "A", stamp = 0
// 线程 T1
int oldStamp = stamp[0];
ref.compareAndSet("A", "C", oldStamp, oldStamp + 1); // CAS 同时检查值和版本号
// 线程 T2: A → B → A (版本号 ++)
ref.compareAndSet("A", "B", 0, 1);
ref.compareAndSet("B", "A", 1, 2);
// 线程 T1 重试时:预期版本号=0,实际版本号=2 → CAS 失败
三、AtomicInteger 源码精读------自旋+CAS 的模板
疑问:AtomicInteger.incrementAndGet() 里面做了什么?如果没有成功怎么办?
回答:核心就是一个 do-while 循环------不断尝试 CAS,直到成功。这叫"自旋"。
3.1 incrementAndGet() 源码
java
public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset; // value 字段的内存偏移量
private volatile int value; // 实际值,volatile 保证可见性
static {
try {
valueOffset = unsafe.objectFieldOffset(
AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
// Unsafe.getAndAddInt()
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset); // 1. 获取当前值(volatile读)
} while (!compareAndSwapInt(o, offset, v, v + delta)); // 2. CAS尝试更新,失败则重试
return v;
}
自旋流程图:
getAndAddInt(obj, offset, 1):
│
└── 循环开始
│
├── 读取当前 value(假设为 5)
│
├── CAS(预期5 → 新值6)
│ │
│ ├── 成功 → 返回 5(旧值)
│ │
│ └── 失败(期间其他线程修改了 value → 例如变成了6)
│ │
│ └── 重新循环:读取新值6 → CAS(预期6 → 新值7)
│ │
│ └── ...
3.2 自旋的优缺点
优点:
- 不需要线程挂起/唤醒(没有内核态切换)
- 在竞争小时效率极高,只比普通赋值多一次 CAS 的开销
缺点:
- 竞争激烈时,大量线程不断自旋,CPU 空转
- 单个共享变量的 CAS 修改会成为瓶颈(所有线程在同一个缓存行上竞争)
什么时候不适合 CAS?
- 竞争非常激烈的场景 → 改用 LongAdder(分散压力)
- 操作复杂(多条指令的复合操作)→ 用 synchronized 或 ReentrantLock
- 需要阻塞等待的场景 → 用 Condition/LockSupport
四、LongAdder 为什么比 AtomicLong 更快?
疑问:高并发下 AtomicLong 性能急剧下降,LongAdder 是怎么解决的?
回答:LongAdder 的核心思想是"分散压力"------将单一计数器拆成多个 Cell,不同线程修改不同的 Cell,最终求和。
4.1 Striped64 的架构
LongAdder 继承自 Striped64:
java
abstract class Striped64 extends Number {
static final class Cell {
volatile long value;
Cell(long x) { value = x; }
}
transient volatile Cell[] cells; // 多个 Cell 的数组
transient volatile long base; // 基础值
transient volatile int cellsBusy; // 扩容锁(CAS 标志)
}
4.2 add() 的执行逻辑
java
// LongAdder.add(long x)
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
// 第一次尝试:CAS 直接更新 base
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
// 找到当前线程对应的 Cell
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended); // Cell 初始化/扩容/兜底
}
}
三步策略:
- 尝试 CAS 更新 base:如果没有 Cell 数组,直接 CAS 修改 base,失败再进入第二步
- 定位 Cell:通过 ThreadLocalRandom 的探针值(probe)哈希到某个 Cell,尝试 CAS 更新该 Cell
- 兜底处理 :如果步骤 2 的 Cell 为空或 CAS 失败,进入
longAccumulate:创建新 Cell、扩容数组、或 CAS 重试
4.3 性能对比
场景:10 个线程各执行 1 千万次 increment()
AtomicLong:
- 所有线程竞争同一个 volatile 变量
- CAS 重试次数多
- 耗时: ~800ms
LongAdder:
- 线程分散到多个 Cell 上
- CAS 几乎不碰撞
- 耗时: ~200ms (快 4 倍)
4.4 LongAdder 的短板
sum() 是弱一致性的:求和时并发的修改可能不被计入。
java
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value; // 遍历累加时,其他线程可能正在修改
}
}
return sum; // 近似值!
}
适用场景:统计 QPS、计数器、累加器等频繁写、偶尔读的场景。如果需要强一致性的精确值,还是用 AtomicLong。
五、其他原子类一览
5.1 AtomicBoolean / AtomicInteger / AtomicLong
基本类型的原子操作,支持 getAndIncrement、compareAndSet 等常用方法。
5.2 AtomicReference
对普通对象的原子更新(CAS 比较的是引用地址)。
java
AtomicReference<User> ref = new AtomicReference<>(new User("张三"));
ref.getAndSet(new User("李四")); // 原子替换
ref.compareAndSet(oldUser, newUser); // CAS 替换
5.3 AtomicIntegerArray / AtomicLongArray / AtomicReferenceArray
数组元素的原子更新:
java
AtomicIntegerArray array = new AtomicIntegerArray(10);
array.incrementAndGet(5); // 第 5 个元素原子自增
5.4 AtomicIntegerFieldUpdater
对已有类的某个 volatile int 字段做原子更新,无需替换整个变量为 AtomicInteger:
java
class User {
volatile int score; // 必须 volatile
}
AtomicIntegerFieldUpdater<User> updater =
AtomicIntegerFieldUpdater.newUpdater(User.class, "score");
updater.incrementAndGet(user); // user.score 原子自增
优势:节省内存(不用为每个字段创建 AtomicInteger 对象)。
六、原子类在 JDK 中的应用
| 组件 | 使用的原子类/CAS | 用途 |
|---|---|---|
| AQS | Unsafe.compareAndSwapInt | 修改 state 状态 |
| ConcurrentHashMap | Unsafe.compareAndSwapObject | 插入新节点、迁移数据 |
| ThreadPoolExecutor | AtomicInteger ctl | 高 3 位存运行状态,低 29 位存线程数 |
| LinkedBlockingQueue | AtomicInteger count | 原子计数,支持双锁并发 |
| LongAdder / Striped64 | CAS + Cell[] | 高并发计数器 |
| CopyOnWriteArrayList | ReentrantLock + 数组复制 |
写时复制(CAS 不适用于数组修改) |
七、常见面试追问
Q1:CAS 是怎么保证可见性的?
CAS 底层
lock cmpxchg指令会锁定缓存行或总线,执行期间在总线上发送信号,使其他 CPU 核心的对应缓存行失效(写回并 invalidate)。CAS 成功后,新值对其他核心立即可见。volatile 的读写也是通过类似机制保证可见性的。
Q2:CAS 可以完全替代 synchronized 吗?
不能。CAS 适用于单变量的原子更新。需要保护多行代码、操作多个变量、或进行复杂判断时,还是要用 synchronized。轻量级锁在竞争不激烈时和 CAS 性能相差不大,竞争激烈时重量级锁的阻塞反而能防止 CPU 空转。
Q3:为什么 AtomicInteger 用 int 作为字段,而不是 long?
它提供了
AtomicLong处理 64 位。如果是 32 位 CPU 上,64 位变量的读写本身不是原子的(需要两个指令),只有 volatile 的 64 位变量才保证原子性。AtomicLong 在 32 位 CPU 上使用 CAS 比较 64 位,可能受限于硬件。
Q4:Unsafe 类的 compareAndSwapInt 和 getAndAddInt 有什么关系?
compareAndSwapInt是 CAS 原子指令的直接封装。getAndAddInt是更上层的封装------内部调用compareAndSwapInt做自旋。AtomicInteger.incrementAndGet()→Unsafe.getAndAddInt(this, valueOffset, 1) + 1→ 循环调用compareAndSwapInt。
总结
原子类全景图
┌──────────────────────────────────────────────────────────────┐
│ 底层:CPU 指令 │
│ x86: lock cmpxchg ARM: ldrex/strex │
└──────────────────────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ JVM 层:Unsafe 类 │
│ compareAndSwapInt(Object o, long offset, int expect, int x) │
└──────────────────────┬───────────────────────────────────────┘
│
┌───────────┼───────────┬──────────────┐
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐
│ AtomicInteger│ │AtomicLong│ │AtomicRef │ │ LongAdder │
│ 自旋+CAS │ │ 自旋+CAS │ │ 自旋+CAS │ │ CAS+Cell分散│
│ 单个变量 │ │ 单个变量 │ │ 单个引用 │ │ 高性能累加 │
└──────────────┘ └──────────┘ └──────────┘ └──────────────┘
核心要点回顾:
- CAS 是一条 CPU 原子指令(
lock cmpxchg),实现"比较+交换"的无锁原子操作 - Java 通过 Unsafe 类调用 CAS,上层封装为 AtomicInteger、AtomicReference 等原子类
- ABA 问题:值从 A 变 B 又变回 A,CAS 无法感知。解决方案:带版本号(AtomicStampedReference)
- 原子类自旋 CAS:do-while 循环重试,竞争小性能极高,竞争大会导致 CPU 空转
- LongAdder 通过 base + Cell[] 分散 CAS 压力,高并发性能显著优于 AtomicLong,但 sum() 是弱一致性
- CAS 适用于单变量原子更新,复杂操作仍需 synchronized 或 Lock
- 原子类在 AQS、ConcurrentHashMap、线程池等框架中被广泛使用
并发系列总结:从第1篇 JMM 与 volatile,到第8篇原子类与 CAS,我们完成了从内存模型到锁机制,从线程管理到并发容器的完整知识体系。Java 并发编程不再是零散的知识点,而是一张互相连接的网。掌握这张网,面试中对任何并发问题都能追本溯源。