大家好,我是晚安code。
这篇我按「并发三大特性」的线给你捋:先讲 JMM 定的规矩,再讲 volatile 为什么只管得动两样、CAS 又是怎么把最后一样补上的。代码我基于 JDK 8 跑过(2026 年 8 月实测),你在 JDK 17+ 上结果一样。面试问这块,看完你能接住追问。
一、JMM:并发安全
并发 bug 的根源,几乎都出在「工作内存和主内存之间拷贝不及时」这八个字上。
JMM(Java 内存模型,Java Memory Model):Java 虚拟机规范里约定的一套内存规则,规定线程怎么读写共享变量、什么时候能看到最新值。你可以把它理解成多线程世界的「交通规则」,不守规矩就要撞车。
JMM 把内存拆成两块:
- 主内存:所有共享变量都住在这里,所有线程共享这一份。
- 工作内存:每个线程一块独享的副本,线程只能在这块副本上动手,不能直接碰主内存。
所以两个线程之间的值传递,永远得绕主内存走一圈。绕得慢,就会读到旧值。
围绕这套「主内存 ⇄ 工作内存」,JMM 还约定了三条同步规则,这也是 synchronized 的语义来源:
- 线程解锁前,必须把共享变量的新值刷回主内存。
- 线程加锁前,必须从主内存把最新值读回工作内存。
- 加锁和解锁得是同一把锁。
图 1 就是主内存和工作内存的关系:两个线程各抱各的副本,唯一的通道就是主内存,线程之间谁也看不见谁的副本。

二、特性①可见性:volatile 的可见性靠什么保证
一个变量不加 volatile,多线程下你可能读到「过期值」——这是并发面试的第一道送分题。
public class StopThreadDemo {
// 去掉 volatile,这个程序会一直空转,停不下来
private static volatile boolean running = true;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
while (running) {
/* 模拟干活 */ }
});
worker.start();
Thread.sleep(1000);
running = false; // 主线程改标记,想让 worker 停下
System.out.println("main 已把 running 改为 false");
}
}
worker 看不见这个改动,是因为它工作内存里的 running 副本还是 true。volatile 干的事就两件:写的时候强制把新值刷回主内存,读的时候强制从主内存重新拿。加上它,这个程序才能正常退出。
volatile 最典型的用法就是「状态开关」:一个线程改、其它线程读。这类「一写多读」场景用它最划算,不加锁也不阻塞。

三、特性②有序性:指令重排与内存屏障
volatile 的第二个能力是禁止指令重排,底层靠内存屏障实现——相当于给代码排队装了两道铁栏杆。
你写的代码,编译器和 CPU 为了提速会偷偷调换执行顺序。单线程无所谓(结果一样),多线程就可能出事。看这个经典例子,a、b、x、y 初始都是 0:
// 线程A: // 线程B:
x = a; y = b;
b = 1; a = 2;
正常跑下来 x=0、y=0。但如果两边都重排成「先写再读」:
// 线程A 重排后: // 线程B 重排后:
b = 1; a = 2;
x = a; y = b;
就可能出现 x=2、y=1 这种按顺序执行永远到不了的怪结果。
内存屏障(Memory Barrier):一条 CPU 指令,作用有两个——保证屏障前后的指令顺序不被调换,顺便强制内存可见性。volatile 的读写前后都会插屏障,把重排的路堵死。

可能有人会问:DCL 单例为什么必须加 volatile?
instance = new Singleton()看着是一行,其实有「分配内存 → 初始化对象 → 指向引用」三步,不加 volatile 可能被重排成先指向引用再初始化,别的线程就会拿到一个半成品。volatile 禁止重排,保证对象完整发布后再被别人看到。
四、特性③原子性:volatile 不保证原子性,CAS 来补
volatile 不保证原子性——num++ 压根不是一条指令,是「读-改-写」三步,中间随时可能被插队。
public class AtomicityDemo {
private static volatile int count = 0;
static void add() {
count++; } // 读 -> 改 -> 写 三步
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) add();
}).start();
}
Thread.sleep(3000); // 简化写法:睡到所有线程跑完
System.out.println("count = " + count); // 永远不是 20000
}
}
我第一次跑这个,count 出来是 18938,怎么都想不通为啥不是 2 万。后来才明白:volatile 只保证「看得见最新值」,不保证「读写这个过程不被切开」。两个线程同一瞬间都读到 count=5,都加 1 再写回,就互相覆盖了一次。

缺口怎么补?两条路:加锁,或者用原子类。
CAS(Compare And Swap,比较并交换):一种无锁的原子操作,只有当前内存值等于我预期的旧值时,才把新值写进去;不等就重试。你可以把它理解成「乐观锁」——先相信没人抢,动手前再验一次货。
把上面 demo 换一行就能修好:
AtomicInteger count = new AtomicInteger(0);
count.getAndIncrement(); // 原子 +1,底层就是 CAS
同样 20 线程跑完,结果稳定 20000。
CAS 的底层是 Unsafe 类。Unsafe 类:Java 留的一个能直接操作内存的「后门」,CAS 的 native 方法都是它提供的。AtomicInteger 的 getAndIncrement 一路调到 Unsafe,核心逻辑就是自旋——比不对就一直重读再比:
// Unsafe.getAndAddInt 简化逻辑
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset); // 1. 读当前值
} while (!compareAndSwapInt(o, offset, v, v + delta)); // 2. 比得上就写,比不上继续转
return v;
}
CAS 的「转圈」长图 6 这样:读 → 比 → 不行就重读,直到成功。

五、CAS 的坑:ABA 问题与原子引用
CAS 有个天然盲区叫 ABA 问题——值绕一圈回到原点,CAS 就以为没人动过,其实中间已经悄悄变了两次。
ABA 问题:线程 1 读到一个值 A,线程 2 把它改成 B 又改回 A,线程 1 拿手里的 A 去比对,发现还是 A,直接更新成功——可中间其实被改过两回了。这就是成语里的「狸猫换太子」。
你品一下这个场景:收货时验货单上写着「货:A」,你验完满意签收。可快递员中途把货换成 B 又换回 A 了——你签收的确实是 A,但中途确实被掉过包。这种「看着没变、实际动过」的情况,CAS 完全看不出来。

解决办法是给值配个版本号,CAS 时值和版本号一起比:
public class AbaFixDemo {
// 值 + 版本号(stamp) 一起管
static AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(2020, 0);
public static void main(String[] args) {
int[] stamp = new int[1];
Integer cur = ref.get(stamp); // 一次拿回「值 + 版本号」
// 别的线程:2020->2021->2020,版本号 0->1->2
ref.compareAndSet(2020, 2021, 0, 1);
ref.compareAndSet(2021, 2020, 1, 2);
// 我们期望版本还是 0,比对失败,ABA 被拦下
System.out.println(ref.compareAndSet(2020, 6666, 0, 3)); // false
}
}
AtomicStampedReference(原子引用):给引用值配一个版本号戳,CAS 时值和版本号都得匹配才更新,用来识别「先变后恢复」的过程变化。你可以理解为「验货时不但看货,还看货单上写的是第几版」。

用它有个坑:别分开调 getReference() 和 getStamp(),两次调用之间状态可能已经变了,要像我上面这样用 get(stampHolder) 一次把值和版本号都拿回来。
可能有人会问:CAS 自旋一直失败会怎样?
会一直空转烧 CPU。自旋是拿 CPU 换等待,竞争激烈时线程一多,一圈圈空转反而更慢,这时候该退回悲观锁(synchronized / Lock)。
六、总结:一张表看清 volatile、CAS、synchronized
volatile、CAS、synchronized 不是三选一,是三个维度各管一摊——能分清谁管什么,面试问并发就不心虚。
| 特性 | volatile | CAS(原子类) | synchronized |
|---|---|---|---|
| 原子性 | 不保证 | 保证(单变量) | 保证 |
| 可见性 | 保证 | 保证(靠底层 volatile) | 保证 |
| 有序性 | 保证(禁重排) | 部分 | 保证 |
| 代价 | 无阻塞 | 自旋可能空转 | 线程阻塞 |
一句话收束:并发安全 = 可见性 + 有序性 + 原子性,JMM 定规则,volatile 管前两样,CAS 补最后一样。下次再被问「谈谈你对 volatile 的理解」,先抛这个框架再往下拆,稳的。

我是晚安code,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:学并发的时候,你是先啃 JMM 还是先背 CAS?有没有在哪一步卡了很久?