• 在 JVM 底层 volatile 是采用“内存屏障”来实现的

  • 缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。其核心思想如下:当某个 CPU 在写数据时,如果发现操作的变量是共享变量,则会通知其他 CPU 告知该变量的缓存行是无效的,因此其他 CPU 在读取该变量时,发现其无效会重新从主存中加载数据

两条原理点到了内存屏障与 MESI,但还差三层把原理闭环:volatile 到底保证了什么(语义)、JMM 如何用 happens-before 表述、以及它为什么不保证原子性。本文补齐。

语义层:两个保证与一个不保证

  • 可见性:对 volatile 变量的写,完成后对其他线程立即可见;读总是读到最新写
  • 有序性:禁止 volatile 读/写与前后普通内存操作重排序(全屏障效果)
  • 不保证原子性:volatile count++ 的读-改-写三步之间仍可被切换,并发下丢更新——这是 volatile 与锁/原子类的根本分界

JMM 表述:happens-before 规则

Java 内存模型用 happens-before 定义可见性,volatile 对应其中一条规则:对 volatile 变量的写 happens-before 于后续对它的读。由此推导经典用法——状态标志:

volatile boolean stop = false;
// 线程A:stop = true; 写
// 线程B:while (!stop) work(); 读:必能看到 A 的写,且 A 在写之前的其他修改也一并可见

注意”一并可见”:volatile 写之前的所有普通写,都随这次写对读者可见(piggyback 效应)——这是它能做”发布开关”的原因。

实现层:内存屏障 + 缓存一致性

JVM 在 volatile 读写两侧插入内存屏障(Memory Barrier):

  • 写后加 StoreLoad 屏障:确保写结果刷出并对其他处理器可见后才允许后续读
  • 读前加 LoadLoad/LoadStore 屏障:确保读到的值之后的操作不会重排到读之前

x86 架构下这些屏障最终落到一条 lock 前缀指令(如 lock addl $0, (%rsp)):lock 前缀做三件事——锁缓存行/总线完成原子写、把本核缓冲区写回主存、使其他核中该缓存行无效。最后一步正是 MESI 协议的介入点:其他核缓存行被标记 Invalid,下次读必须从主存/他核重新加载——可见性由此在硬件层闭环。

所以原文两条的衔接是:内存屏障是 JVM 发出的指令级约束,MESI 是硬件执行可见性的协议;前者指挥,后者落地。

有序性的实战价值:安全发布与 DCL

指令重排让 obj = new Object() 可能以”分配→引用赋值→构造”的顺序执行(2、3 重排)。未加 volatile 的 DCL 单例可能返回半初始化对象;volatile 修饰实例字段后禁止该重排,DCL 才正确:

private static volatile Singleton instance;
public static Singleton get() {
if (instance == null) synchronized (Singleton.class) {
if (instance == null) instance = new Singleton(); // volatile 禁止构造与赋值重排
}
return instance;
}

适用与禁用清单

适用:

  • 状态标志位(stop/ready)协调线程
  • 安全发布(一次性发布不可变或初始化完成的对象引用)
  • DCL 单例
  • 独立变量的”写一次读多次”场景

禁用/无效:

  • 复合操作(count++、check-then-act)→ 用 atomic/锁
  • 多变量间的不变式(a<b 约束跨两个 volatile 变量仍会破坏)→ 锁保护整体
  • 替代同步做互斥 → volatile 没有互斥语义

与 atomic 的关系

AtomicInteger 内部正是 volatile 字段 + CAS:volatile 保证可见性,CAS 保证读-改-写的原子性。理解这层组合,就明白”volatile 是原子类的地基之一,但单独使用只有一半能力”。

volatile 的原理链条:JMM 语义(可见+有序)→ 内存屏障(指令约束)→ lock 前缀 + MESI(硬件落地)。记住它”管可见有序、不管原子”,使用边界就再不会错。