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 程序员视角的判定方式

判断多线程代码是否安全,常用四步:

  1. 找出所有共享变量的读写。
  2. 在读写之间建立 happens-before 链
  3. 若「写」能 happens-before 到「读」,则读一定能看到写的内容(以及写之前、对该写线程可见的所有操作)。
  4. 若链断了,就存在数据竞争或未定义行为风险。

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 的对比

  • synchronizedunlock hb 后续 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 --> cpu

6.1 字节码层:volatile 如何被标记

Java 源码中的 volatile 字段 → class 文件 ACC_VOLATILE 标志 → 运行时 getfield / putfieldvolatile 分支,而非普通 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_acquireacquire
普通写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 --> c3

volatile 读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 writes

6.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,形成三档访问语义。

AtomicIntegerjdk/.../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 节字节码路径;lazySetputOrderedInt,语义弱于 volatile 写(仅 StoreStore,不保证立即可见)。

AbstractQueuedSynchronizerjdk/.../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"]
    end

FutureTask 的工程取舍jdk/.../concurrent/FutureTask.java

} finally { // final state
    UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
}

解释:状态机进入终态时,只需把写与之前的 store 排序(StoreStore),不要求立即对所有线程可见;putOrderedInt 足够,成本低于 full volatile 写。

6.7 四层如何对应 JMM

对应 JMM 概念JDK 8 实现载体
JMMvolatile 写 hb 后续读;写前操作通过传递性可见语言规范
字节码标记 volatile 访问ACC_VOLATILE + getfield/putfield
VMacquire 读 / 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 总结

  1. JMM 定义跨线程可见性规则;happens-before 是判定「读能否看到写」的工具——有 hb 链则可见,无 hb 链则不保证。
  2. volatile 在 JMM 中提供可见性与单次读写的有序性,不保证复合操作的原子性,也不能替代锁的互斥语义。
  3. HotSpot 将 volatile hb 规则落实为 acquire 读、release 写、StoreLoad 屏障,解释器与 C2 JIT 路径不同但语义一致,最终下沉到 OrderAccess 与平台指令。
  4. JUC 的典型模式是 volatile 字段 + CAS(Unsafe),必要时用 putOrdered 等更弱语义做工程取舍。

理解 volatile,需要同时看规范层(JMM / happens-before)与实现层(字节码 → VM → 平台)。只看其一,很容易把它误当成「轻量锁」或「万能线程安全注解」。