从 volatile 到 CAS,再到 AtomicInteger 源码
静下心来,把 JDK 源码翻了一遍,发现 JUC 的地基其实很清晰——volatile 保证可见性,CAS 保证原子性,Unsafe 是底层操作入口。
这篇文章,把 JUC 学习的第一和第二阶段串起来讲,带源码分析。
一、先搞懂 JMM:并发问题的根源
1.1 为什么会有并发问题?
CPU 太快,内存太慢。所以 CPU 加了一层缓存:
┌─────────────────────────────────────────┐
│ 主内存 │
│ 共享变量存在这里 │
└───────────────┬─────────────────────────┘
│
┌───────▼───────┐
│ CPU 缓存 │ ← 线程读写的是缓存,不是主内存
│ (工作内存) │
└───────────────┘
问题来了:
线程 A 修改了变量 flag = true,写到了 CPU 缓存里,还没来得及写回主内存。
线程 B 此时读 flag,读的是自己缓存里的旧值,看到的还是 false。
这就是可见性问题。
1.2 JMM 的抽象
JMM(Java Memory Model)是 Java 规范定义的抽象概念,屏蔽了不同硬件的内存模型差异。
它规定了两条核心规则:
- 可见性:一个线程对共享变量的修改,其他线程能看见
- 有序性:指令重排不能影响执行结果
二、volatile:JMM 的钥匙
2.1 volatile 能做什么?
volatile boolean flag = false;
// 线程 A
flag = true;
// 线程 B
while (!flag) {
// 等待
}
System.out.println("flag 变成 true 了");
加了 volatile 之后,线程 B 一定能读到线程 A 修改后的值。
volatile 保证两点:
- 可见性:写完立刻刷回主内存,读完立刻从主内存加载
- 有序性:禁止指令重排
2.2 volatile 不保证什么?
volatile int count = 0;
// 线程 A
count++; // 读取 → 加1 → 写回
// 线程 B
count++; // 同时也在做读取 → 加1 → 写回
两个线程都执行了 count++,但最终结果可能是 1,不是 2。
因为 count++ 是三步操作(读-改-写),volatile 只能保证单次读/写的可见性,复合操作不保证原子性。
2.3 volatile 的底层实现:内存屏障
JVM 在 volatile 读写时会插入内存屏障:
volatile boolean flag = true; // 写
编译成汇编会多一条指令:
mov BYTE PTR [rax+0x68], 0x1 ; 写入值
lock addl $0x0, (%rsp) ; LOCK 前缀 = 内存屏障
LOCK 前缀做了什么?
LOCK 前缀
│
├─ 强制将 CPU 缓存的数据写回主内存
│
├─ 通过 MESI 协议使其他 CPU 缓存行失效
│ (线程 B 下次读必须从主内存重新加载)
│
└─ 禁止指令重排(StoreLoad 屏障)
Happens-Before 规则:对 volatile 变量的写,happens-before 后续对该变量的读。
// 线程 A
sharedData = 42; // 普通写
flag = true; // volatile 写
// 线程 B
while (!flag) {} // volatile 读
System.out.println(sharedData); // 一定输出 42
重点:sharedData 的修改对线程 B 可见,因为 volatile 的写 happens-before 规则保证了顺序。
2.4 面试高频追问:单例模式为什么要加 volatile?
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
instance = new Singleton() 分三步:
1. 分配内存
2. 调用构造方法初始化对象
3. 将 instance 指向这块内存
没有 volatile,步骤 2 和 3 可能重排。
线程 A 执行到步骤 3(还没执行步骤 2),线程 B 刚好进入 getInstance(),发现 instance 不为 null,直接返回了一个未初始化完成的对象。
volatile 禁止了步骤 2 和 3 重排,所以必须加。
三、CAS:无锁并发的核心
3.1 CAS 是什么?
CAS(Compare And Swap,比较并交换)是 CPU 指令级别的原子操作。
CAS(内存地址, 期望值, 新值)
→ 若地址的值 == 期望值,将地址的值改成新值,返回 true
→ 否则什么都不做,返回 false
整个过程是原子的,CPU 保证。
3.2 CAS 在 x86 上的实现
LOCK CMPXCHG [地址], 新值
CMPXCHG:比较寄存器 EAX 和内存地址的值- 相等:ZF=1,把新值写入内存
- 不等:ZF=0,把内存值加载到 EAX
LOCK前缀:多核环境下锁定总线,保证原子性
3.3 CAS 的自旋模式
CAS 不阻塞线程,失败了怎么办?重来。
// getAndAddInt 底层实现
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset); // 读取当前值
} while (!compareAndSwapInt(o, offset, v, v + delta)); // CAS 失败就重试
return v;
}
死循环 + CAS,就是无锁并发的核心模式。
四、Unsafe 类:绕过 JVM 的后门
4.1 Unsafe 是什么?
sun.misc.Unsafe 提供了直接操作内存的能力,CAS 就是通过它暴露给 Java 的。
Unsafe 不能直接 new,需要反射获取:
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
4.2 Unsafe 的 CAS 方法
// 三个核心 native 方法
public final native boolean compareAndSwapInt(
Object obj, // 目标对象
long offset, // 字段内存偏移量
int expected, // 期望值(旧值)
int update // 新值
);
public final native boolean compareAndSwapLong(
Object obj, long offset, long expected, long update);
public final native boolean compareAndSwapObject(
Object obj, long offset, Object expected, Object update);
这些方法都是 native 的,JVM 底层调用 C++,C++ 调用汇编指令。
4.3 字段偏移量:怎么知道值存在哪?
JVM 中每个对象在内存里的布局是固定的。Unsafe.objectFieldOffset() 能算出字段相对于对象头的偏移量:
private static final long valueOffset;
static {
valueOffset = unsafe.objectFieldOffset(
AtomicInteger.class.getDeclaredField("value"));
}
拿到偏移量,Unsafe 就能直接操作这个位置的值。
五、AtomicInteger 源码:CAS 的完整应用
5.1 核心结构
public class AtomicInteger extends Number implements java.io.Serializable {
// Unsafe 实例,CAS 操作的入口
private static final Unsafe unsafe = Unsafe.getUnsafe();
// value 字段在内存中的偏移量
private static final long valueOffset;
static {
try {
// 类加载时算出偏移量,后续操作直接用
valueOffset = unsafe.objectFieldOffset(
AtomicInteger.class.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
// volatile 保证可见性
private volatile int value;
// ...
}
5.2 incrementAndGet:自增操作
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
调的还是 Unsafe 的 getAndAddInt,自旋 CAS 实现。
5.3 compareAndSet:CAS 更新
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
直接调用 Unsafe 的 CAS,语义就是"期望值等于当前值,才更新成新值"。
5.4 getAndAccumulate:自定义累加
public final int getAndAccumulate(int x, IntBinaryOperator func) {
int prev, next;
do {
prev = get(); // 读取当前值
next = func.applyAsInt(prev, x); // 计算新值
} while (!compareAndSet(prev, next)); // CAS 失败重试
return prev;
}
这里有个容易忽略的细节:自旋 CAS 在高并发下可能消耗大量 CPU。
六、ABA 问题:C最讨厌的坑
6.1 ABA 是什么?
T1 读取值 A
T2 把 A 改成 B
T2 把 B 改回 A ← 值变回来了
T1 做 CAS(A, C),成功 ← 但中间已经变过
CAS 只比较值,不记录"中间变过"。
6.2 ABA 的危害场景
// 栈的 pop 操作
// 初始:栈顶是 A
Node top = stack.top;
// 在 T1 执行 CAS 之前,T2 做了:
// 1. pop A → top = B
// 2. push 新节点 A → top = A
// 此时值又变回 A,但 A 已经不是原来的 A 了
// T1 执行 CAS(top, A, B),成功
// 但此时 B 可能已经被 T2 pop 过,栈结构已经损坏
6.3 解决方案:加版本号
// AtomicStampedReference:值 + 版本号
AtomicStampedReference<String> ref =
new AtomicStampedReference<>("A", 1);
// T1:读取值和版本号
int[] stamp = new int[1];
String val = ref.get(stamp); // val="A", stamp=1
// T2:A→B→A,版本号变成 2→3
ref.compareAndSet("A", "B", 1, 2);
ref.compareAndSet("B", "A", 2, 3);
// T1:CAS,版本号不匹配,失败
ref.compareAndSet("A", "C", 1, 2); // false
加了版本号,CAS 同时比较值和版本号,中间任何变化都会被检测到。
6.4 两种解决方案对比
| 方案 | 数据结构 | 适用场景 |
|---|---|---|
AtomicStampedReference |
值 + int 版本号 | 精确追踪修改次数 |
AtomicMarkableReference |
值 + boolean 标记 | 只需知道"改没改过" |
七、踩坑总结
坑一:volatile 不是万能的
volatile 只能保证单次读写,复合操作(count++)不保证原子性。
// ❌ 错误
volatile int count = 0;
count++; // 不安全
// ✅ 正确
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
坑二:自旋 CAS 的 CPU 消耗
高并发下大量线程自旋重试,CPU 100%。
解决方案:控制自旋次数,或者用 LongAdder(分段 CAS)替代 AtomicLong。
坑三:ABA 问题容易被忽略
链表操作、栈操作等场景下,ABA 会导致数据结构损坏。
解决方案:用 AtomicStampedReference 替代 AtomicReference。
八、知识串联
并发问题的根源:CPU 缓存导致可见性、指令重排导致有序性
JMM 规范:happens-before 规则定义了"什么样的顺序是合法的"
volatile:
└─ 通过 LOCK 前缀 + MESI 协议,保证可见性 + 有序性
└─ 不保证复合操作的原子性
CAS:
└─ CPU 指令级别的原子操作,失败就重试
└─ 底层靠 Unsafe 暴露给 Java
Unsafe:
└─ 直接操作内存,CAS 的 Java 入口
└─ native 方法,JVM 底层调汇编
AtomicInteger:
└─ volatile value + Unsafe CAS + 自旋
└─ 完整展示 CAS 的实际应用
ABA 问题:
└─ 值相同但中间被改过
└─ 用 AtomicStampedReference 加版本号解决
九、面试 Q&A
Q1:volatile 和 synchronized 的区别?
| volatile | synchronized | |
|---|---|---|
| 原子性 | ❌ 不保证复合操作 | ✅ 保证 |
| 可见性 | ✅ 立即刷新主内存 | ✅ happens-before |
| 有序性 | ✅ 禁止指令重排 | ✅ 互斥保证有序 |
| 性能 | 高(无锁) | 低(加锁) |
Q2:CAS 的缺点?
- 自旋消耗 CPU
- 只能保证单变量原子性
- 存在 ABA 问题
Q3:AtomicInteger.incrementAndGet() 怎么实现的?
Unsafe 的 getAndAddInt,底层是自旋 CAS:读取当前值 → CAS 更新 → 失败重试。
Q4:ABA 问题怎么解决?
AtomicStampedReference 加版本号,或者 AtomicMarkableReference 加标记位。
十、口诀
JMM 三问题:可见性、原子性、有序性
volatile 保可见保有序,不保复合原子性
CAS 是 CPU 指令,失败就重试
Unsafe 是后门,native 调汇编
AtomicInteger = volatile + CAS + 自旋
ABA 问题不忽视,版本号来标记
搞懂 volatile 和 CAS,JUC 就入门了一半。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)