volatile 解决的是多线程下的可见性与有序性,不保证复合操作的原子性。在 JMM 里它对应 happens-before 规则;在 HotSpot 里它落实为 acquire/release 语义的字段访问与 StoreLoad 屏障——不是锁的轻量替代品。
1 问题背景:为什么普通字段不够
多线程读写同一个普通字段时,一个线程的写入,另一个线程可能长期看不到。原因不在 Java 语法本身,而在底层允许的重排序与缓存:编译器、JIT、CPU 都可能让「程序顺序」与「其他线程观察到的顺序」不一致。
下面是一个极简反例(不保证一定复现,但 JMM 允许这种行为):
class VisibilityDemo {
static boolean ready = false; // 普通字段,无同步
static int value = 0;
public static void main(String[] args) throws Exception {
new Thread(() -> {
value = 42;
ready = true;
}).start();
while (!ready) { /* spin */ }
System.out.println(value); // 可能输出 0
}
}
读者容易犯的误区是:把源码里的先后顺序,等同于「写 value 一定先于写 ready 对其他线程可见」。无同步时,JMM 不保证这一点。
我们需要一套跨线程的可见性规则——这就是 JMM(Java Memory Model) 要解决的问题。volatile 是 JMM 提供的同步机制之一;理解它之前,先把 JMM 和 happens-before 建立起来。
2 JMM 是什么
2.1 定义与定位
JMM(Java Memory Model,Java 内存模型) 是 JLS / JSR-133 定义的一套多线程内存可见性与有序性规则。它回答:
- 线程 A 对共享变量的写,线程 B 在什么条件下一定能看到?
- 编译器、JIT、CPU 可以对内存操作做哪些重排序?
关键区分:
- 程序顺序:源码 / 字节码里的先后。
- 内存操作的生效顺序:其他线程能观察到的先后。
JMM 约束的是后者——跨线程可见性的合法边界。它不是 JVM 源码里的某个类,而是语言层面的规范。
2.2 JMM 管什么、不管什么
| JMM 管 | JMM 不管 |
|---|---|
| 普通字段、volatile、final 的可见性与有序性 | 单线程内的语义(由语言语义保证) |
| synchronized、volatile、线程 start/join 等的 happens-before | 锁的实现细节(偏向锁、膨胀等) |
| 哪些重排序对程序员是「不允许」的 | CPU 缓存一致性协议的具体实现 |
2.3 主内存与工作内存
JMM 用抽象模型描述多线程访问:
flowchart LR
mainMem["主内存 shared variables"]
t1["线程1 工作内存"]
t2["线程2 工作内存"]
mainMem <-->|"read / write copy"| t1
mainMem <-->|"read / write copy"| t2- 共享变量存在于主内存。
- 每个线程有自己的工作内存(本地缓存 / 寄存器的抽象)。
- 线程读写共享变量,实质是主内存与工作内存之间的拷贝与回写。
无同步时,JMM 允许不同线程的工作内存长期不一致——第 1 节反例的根源即在于此。JMM 通过 happens-before 等规则,约束「何时必须把本地修改刷出去、何时必须读最新值」。
2.4 与 volatile 的关系
volatile 是 JMM 提供的同步机制之一。理解 volatile 的前提,是先理解 JMM 如何用 happens-before 定义「可见」。
3 happens-before 是什么
3.1 定义
happens-before 是 JMM 中的偏序关系(partial order),记作 A happens-before B(或 A → B)。
含义:
- 若 A happens-before B,则 A 的执行结果对 B 可见;
- 且 A 在感知顺序上排在 B 之前。
注意:这不是物理时间上的「谁先执行完」,而是 JMM 保证的可见性顺序。两个操作可以没有 happens-before 关系——此时 JMM 不保证谁先谁后、是否可见。
3.2 程序员视角的判定方式
判断多线程代码是否安全,常用四步:
- 找出所有共享变量的读写。
- 在读写之间建立 happens-before 链。
- 若「写」能 happens-before 到「读」,则读一定能看到写的内容(以及写之前、对该写线程可见的所有操作)。
- 若链断了,就存在数据竞争或未定义行为风险。
3.3 JMM 内置的 happens-before 规则
与 volatile 相关的 subset:
| 规则 | 含义 |
|---|---|
| 程序顺序规则 | 同一线程内,前面的操作 hb 后面的操作 |
| 监视器锁规则 | unlock hb 后续对同一把锁的 lock |
| volatile 规则 | 对 volatile 变量 V 的写 hb 后续任意线程对 V 的读 |
| 线程启动规则 | Thread.start() hb 该线程内的任意操作 |
| 线程终止规则 | 线程内任意操作 hb 其他线程检测到该线程终止(如 join 返回) |
| 传递性 | A hb B 且 B hb C,则 A hb C |
传递性示例:volatile 写可以把之前的普通写「带出去」。
class HappensBeforeDemo {
int x = 0;
volatile boolean flag = false;
void writer() {
x = 1; // ①
flag = true; // ② volatile 写
}
void reader() {
if (flag) { // ③ volatile 读
int r = x; // ④ 一定能看到 x == 1
}
}
}
flowchart LR
w1["Thread1: x = 1"]
w2["Thread1: volatile flag = true"]
r1["Thread2: read flag"]
r2["Thread2: read x"]
w1 --> w2
w2 -->|"volatile 写 hb 读"| r1
r1 -->|"程序顺序 + 传递性"| r2推理链:① hb ②(程序顺序)→ ② hb ③(volatile 规则)→ ③ hb ④(程序顺序)→ 由传递性得 ① hb ④,故 reader 读 x 时一定看到 1。
3.4 happens-before 不保证什么
- 没有 hb 关系的两次写,其他线程可能以任意顺序观察到。
- hb 保证可见性,不自动保证复合操作的原子性(例如 check-then-act 若跨多个变量且无单一 hb 链覆盖,仍不安全)。
- 与物理时钟无关:A 在墙上时钟上早于 B,不代表 A hb B。
3.5 与 synchronized 的对比
synchronized:unlockhb 后续lock,且锁内操作对后续获锁线程可见;提供互斥。volatile:单次写 hb 后续读,无互斥。
详细能力对比见第 4 节。
4 volatile 在 JMM 中保证什么、不保证什么
建立在第 2、3 节之上,volatile 的 JMM 语义可以归纳为三条。
可见性:对 volatile 变量 V 的写,happens-before 于后续任意线程对 V 的读(第 3.3 节 volatile 规则)。
有序性(禁止特定重排序):
- volatile 写之前的操作,对该写 hb(程序顺序),写又 hb 后续读 → 形成传递链,写前的修改随 volatile 写一起可见。
- volatile 读之后的操作,不能重排到读之前(否则破坏 hb 链)。
不保证原子性:读-改-写仍是两步。例如:
volatile int count = 0;
void increment() {
count++; // 读 count → 加 1 → 写 count,非原子
}
两个线程各调一次 increment(),count 可能只增加 1。需要 AtomicInteger 或锁。
volatile 不能替代锁:它没有互斥、不能保护复合不变式、不支持 wait/notify。它适合单次写、多处读的状态发布,或作为 CAS 结构的底层可见字段。
5 典型工程场景
| 场景 | 为什么用 volatile | 注意点 |
|---|---|---|
| 状态标志位 | 一次写、多处读,只需可见 | 复合状态变更仍需锁 |
双重检查锁(DCL)单例的 instance | 安全发布已构造完成的对象引用 | 必须配合 synchronized |
| JUC 共享状态字段 | CAS / 队列的底层可见载体 | 常与 Unsafe 配合,见第 6.6 节 |
5.1 双重检查锁:为何 instance 必须 volatile
DCL 中,若 instance 不是 volatile,其他线程可能在对象尚未完全初始化时就看到非 null 引用。
private static volatile CipherTestUtils instance = null;
public static CipherTestUtils getInstance() throws Exception {
if (instance == null) {
synchronized (CipherTestUtils.class) {
if (instance == null) {
instance = new CipherTestUtils();
}
}
}
return instance;
}
解释:外层读 instance 无锁,必须 volatile,保证看到 synchronized 块内写入的最新引用;内层 synchronized 保证只构造一次。volatile 在这里不是锁,而是与锁配合完成安全发布。
6 从 JMM 到机器指令:volatile 的四层实现栈
第 3 节的 volatile hb 规则,在 HotSpot 里逐层落实为 acquire/release 语义与 StoreLoad 屏障。
flowchart TB
jmm["Layer1: JMM volatile 写 hb 后续读"]
bytecode["Layer2: getfield/putfield 字节码"]
vm["Layer3: HotSpot 解释器 或 C2 JIT"]
cpu["Layer4: OrderAccess 平台实现 x86 lock/fence"]
jmm --> bytecode
bytecode --> vm
vm --> cpu6.1 字节码层:volatile 如何被标记
Java 源码中的 volatile 字段 → class 文件 ACC_VOLATILE 标志 → 运行时 getfield / putfield 走 volatile 分支,而非普通 int_field / int_field_put。
字节码层只负责区分「这是 volatile 访问」;屏障语义由 VM 注入。
6.2 解释器路径
HotSpot 解释执行 _getfield / _putfield 时,通过常量池缓存项 cache->is_volatile() 分支处理。
volatile 读 — hotspot/.../bytecodeInterpreter.cpp
if (cache->is_volatile()) {
if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
OrderAccess::fence();
}
if (tos_type == atos) {
SET_STACK_OBJECT(obj->obj_field_acquire(field_offset), -1);
} else if (tos_type == itos) {
SET_STACK_INT(obj->int_field_acquire(field_offset), -1);
}
// ... 其他类型同理
} else {
SET_STACK_INT(obj->int_field(field_offset), -1);
// ... 普通字段无 acquire
}
解释:volatile 读走 *_field_acquire(acquire 读);普通读走 *_field。部分 CPU 上在读前额外 fence()(与 IRIW 相关,正文不展开)。
volatile 写 — 同文件
if (cache->is_volatile()) {
if (tos_type == itos) {
obj->release_int_field_put(field_offset, STACK_INT(-1));
} else if (tos_type == atos) {
obj->release_obj_field_put(field_offset, STACK_OBJECT(-1));
}
// ... 其他类型同理
OrderAccess::storeload();
} else {
obj->int_field_put(field_offset, STACK_INT(-1));
// ... 普通写无 release / storeload
}
解释:volatile 写用 release_*_field_put(release 写),写后调用 OrderAccess::storeload(),对应 JSR-133 的 StoreLoad 屏障——保证写对其他线程的 load 可见顺序。
| 访问类型 | 解释器 API | 屏障 |
|---|---|---|
| 普通读 | int_field | 无 |
| volatile 读 | int_field_acquire | acquire |
| 普通写 | int_field_put | 无 |
| volatile 写 | release_int_field_put + storeload() | release + StoreLoad |
6.3 C2 JIT 路径
热点方法被 C2 编译后,在 IR 层插入 MemBar 节点,表达与解释器相同的 JMM 约束。
flowchart LR
subgraph interp ["解释器"]
i1["getfield volatile"]
i2["field_acquire + storeload"]
end
subgraph c2 ["C2 JIT"]
c1["parse3.cpp"]
c2a["MemBarAcquire / MemBarRelease"]
c2b["MemBarVolatile StoreLoad"]
c3["OrderAccess 平台指令"]
end
i1 --> i2 --> c3
c1 --> c2a --> c2b --> c3volatile 读 — hotspot/.../opto/parse3.cpp
MemNode::MemOrd mo = is_vol ? MemNode::acquire : MemNode::unordered;
Node* ld = make_load(NULL, adr, type, bt, adr_type, mo, ...);
if (field->is_volatile()) {
Node* mb = insert_mem_bar(Op_MemBarAcquire, ld);
mb->as_MemBar()->set_trailing_load();
}
解释:load 带 MemNode::acquire 语义;随后 MemBarAcquire 阻止后续内存操作上浮越过 volatile 读。
volatile 写 — 同文件
if (is_vol) {
leading_membar = insert_mem_bar(Op_MemBarRelease);
}
const MemNode::MemOrd mo = is_vol ? MemNode::release : ...;
Node* store = store_to_memory(..., mo, is_vol);
if (is_vol) {
if (!support_IRIW_for_not_multiple_copy_atomic_cpu) {
Node* mb = insert_mem_bar(Op_MemBarVolatile, store);
MemBarNode::set_store_pair(leading_membar->as_MemBar(), mb->as_MemBar());
}
}
解释:写前 MemBarRelease 阻止前面的操作下潜越过 volatile 写;写后 MemBarVolatile(fat barrier,含 StoreLoad)在 x86 常见路径上生效。解释器用 acquire/release API + storeload;C2 用 MemBar 节点——语义同源,载体不同,最终都下沉到 OrderAccess。
6.4 OrderAccess:JSR-133 到 HotSpot 的统一抽象
hotspot/.../runtime/orderAccess.hpp
// This interface is based on the JSR-133 Cookbook for Compiler Writers
// ...
// StoreLoad: Store1(s); StoreLoad; Load2
//
// Ensures that Store1 completes before Load2 and any subsequent load
// operations. Stores before Store1 may *not* float below Load2 ...
// Execution by a processor of release makes the effect of all memory
// accesses issued by it previous to the release visible to all
// processors *before* the release completes.
// Execution by a processor of acquire makes the effect of all memory
// accesses issued by it subsequent to the acquire visible to all
// processors *after* the acquire completes.
同文件注释中的平台对照表指出:在 x86 上,full fence 常实现为 lock addl 0,(sp);StoreLoad 是四类屏障中最贵的一种。
sequenceDiagram
participant TA as ThreadA
participant Mem as 主内存
participant TB as ThreadB
TA->>Mem: release_store volatile 写
Note over TA,Mem: StoreStore + 写完成
TA->>Mem: storeload 屏障
TB->>Mem: load_acquire volatile 读
Note over TB,Mem: 读到最新值及 hb 链上的 prior writes6.5 平台层:x86 上屏障如何变成机器指令
hotspot/.../os_cpu/linux_x86/vm/orderAccess_linux_x86.inline.hpp
inline void OrderAccess::storeload() { fence(); }
inline void OrderåAccess::acquire() {
volatile intptr_t local_dummy;
__asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
}
inline void OrderAccess::fence() {
if (os::is_MP()) {
// always use locked addl since mfence is sometimes expensive
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
}
}
解释:
storeload()委托给fence();多处理器下使用lock; addl $0,0(%rsp)(注释说明 mfence 有时更贵)。acquire()/release()通过带"memory"clobber 的内联汇编,阻止编译器重排。- x86 TSO 已较强,但 StoreLoad 仍需要 full fence 类指令。ARM 等平台会用
dmb/ldar/stlr等等价实现(平台相关,本文不展开)。
6.6 Java 层与 Unsafe:JUC 的三档语义
JUC 大量在 volatile 字段之上使用 Unsafe,形成三档访问语义。
AtomicInteger — jdk/.../atomic/AtomicInteger.java
private volatile int value;
public final int get() {
return value;
}
public final void set(int newValue) {
value = newValue;
}
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
解释:get / set 直接读写 volatile 字段,走第 6.2 节字节码路径;lazySet 用 putOrderedInt,语义弱于 volatile 写(仅 StoreStore,不保证立即可见)。
AbstractQueuedSynchronizer — jdk/.../locks/AbstractQueuedSynchronizer.java
private volatile int state;
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
解释:state 是 volatile 字段;getState / setState 文档标注 volatile 读写语义;状态变迁的原子更新走 CAS,而非依赖 volatile 保证 i++ 类复合操作。
Unsafe 三档 — jdk/.../sun/misc/Unsafe.java
public final native boolean compareAndSwapInt(Object o, long offset,
int expected, int x);
public native int getIntVolatile(Object o, long offset);
public native void putIntVolatile(Object o, long offset, int x);
public native void putOrderedInt(Object o, long offset, int x);
flowchart TB
subgraph full ["完整 volatile 语义"]
g1["getIntVolatile / putIntVolatile"]
end
subgraph weak ["较弱有序"]
g2["putOrderedInt lazySet"]
end
subgraph atomic ["原子 + volatile 读写"]
g3["compareAndSwapInt"]
endFutureTask 的工程取舍 — jdk/.../concurrent/FutureTask.java
} finally { // final state
UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
}
解释:状态机进入终态时,只需把写与之前的 store 排序(StoreStore),不要求立即对所有线程可见;putOrderedInt 足够,成本低于 full volatile 写。
6.7 四层如何对应 JMM
| 层 | 对应 JMM 概念 | JDK 8 实现载体 |
|---|---|---|
| JMM | volatile 写 hb 后续读;写前操作通过传递性可见 | 语言规范 |
| 字节码 | 标记 volatile 访问 | ACC_VOLATILE + getfield/putfield |
| VM | acquire 读 / release 写 / StoreLoad | 解释器 field_acquire API;C2 MemBar 节点 |
| 平台 | 禁止 CPU/编译器重排 | OrderAccess → x86 lock/fence 等 |
7 边界与常见误区
误区 1:volatile 能保证 count++ 原子
不能。count++ 是读-改-写三步,volatile 只保证每一步本身的可见/有序,不保证三步作为整体不被打断。应使用 AtomicInteger 或锁。
误区 2:volatile 可以保护对象内部字段
volatile 只保证该字段本身(引用或基本类型值)的可见性。若字段是对象引用,volatile 保证引用地址可见,不保证引用指向对象内部字段的复合不变式;对象内部字段仍需独立同步。
误区 3:把 volatile 当性能更好的锁
volatile 无互斥、无阻塞、无 wait/notify。多个线程同时写 volatile 变量,仍会发生丢失更新(除非配合 CAS)。
误区 4:混淆 volatile 与 final 的发布语义
final 字段在构造器完成后对任意线程安全发布(final 安全发布规则);volatile 是每次读写的 hb 边界。二者解决的问题不同,不能互换。
补充:JDK 5 起对 long/double 的读写本身已原子,但可见性仍无保证——非 volatile 的 64 位字段在多线程下仍可能读到过期值。
8 总结
- JMM 定义跨线程可见性规则;happens-before 是判定「读能否看到写」的工具——有 hb 链则可见,无 hb 链则不保证。
- volatile 在 JMM 中提供可见性与单次读写的有序性,不保证复合操作的原子性,也不能替代锁的互斥语义。
- HotSpot 将 volatile hb 规则落实为 acquire 读、release 写、StoreLoad 屏障,解释器与 C2 JIT 路径不同但语义一致,最终下沉到 OrderAccess 与平台指令。
- JUC 的典型模式是 volatile 字段 + CAS(Unsafe),必要时用
putOrdered等更弱语义做工程取舍。
理解 volatile,需要同时看规范层(JMM / happens-before)与实现层(字节码 → VM → 平台)。只看其一,很容易把它误当成「轻量锁」或「万能线程安全注解」。