Skip to content

08.2 - volatile 的内存语义

定位: volatile 是 Java 提供的最轻量级同步机制——保证单个变量的可见性和禁止重排序,但不保证原子性。本质是通过内存屏障操作缓存一致性协议。 面试高频度: ⭐⭐⭐⭐⭐ 考查方式: volatile 的两层语义、DCL 为什么要 volatile、volatile 能保证原子性吗、内存屏障的插入策略

一、这是什么?为什么需要它?

问题:没有 volatile,共享变量不可预期

CPU Core 0 (Thread A)           CPU Core 1 (Thread B)
┌────────────────────┐         ┌────────────────────┐
│  write x = 42      │         │  read x            │
│      ↓              │         │      ↓              │
│  L1 Cache: x=42    │         │  L1 Cache: x=0 ← 旧值!
│  (已更新)            │         │  (未收到更新通知)    │
│      ↓              │         │      ↓              │
│  Store Buffer       │         │  Thread B 读到 x=0  │
│  (可能还没 flush)    │         │  (完全不知道 A 写了) │
└────────────────────┘         └────────────────────┘

硬件层面:MESI 缓存一致性协议
────────────────────────────
Store Buffer 优化导致"写后立刻对其他核心不可见"
→ 缓存一致性协议需要时间传播
→ volatile = 强制插入内存屏障 + 清空 Store Buffer

WHY volatile:每个 CPU 核心的缓存各自为政。Thread A 写一个变量,这个写操作可能停留在 A 的 Store Buffer 或 L1 缓存里,Thread B 从自己的缓存读到的是旧值。volatile 强制两个语义:

  1. 可见性:volatile 写 → 立即 flush 到主存 + 无效化其他核心的缓存行
  2. 禁止重排序:volatile 读/写周围不能有指令重排序

二、原理拆解

2.1 语义一:可见性 (Visibility)

volatile 如何保证可见性?

volatile 写的 CPU 级操作:
┌─────────────────────────────────────────────────────────────────┐
│                                                                  │
│  ① 写入 volatile 变量                                           │
│  ② 执行 StoreLoad 内存屏障 (mfence / lock addl 指令)              │
│  ③ 清空 Store Buffer, 确保写入结果进入缓存/主存                    │
│  ④ 发送缓存一致性消息 (MESI: 修改→其他核心的缓存行无效化)           │
│                                                                  │
│  volatile 读的 CPU 级操作:                                       │
│                                                                  │
│  ① 其他核心的缓存行已被无效化 (由 volatile 写的 MESI 广播保证)     │
│  ② 从主存重新读取 (或从提供缓存行的核心读取)                       │
│  ③ 执行 LoadLoad + LoadStore 屏障, 防止后续指令重排               │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

MESI 缓存一致性协议的作用

MESI 状态 (每个缓存行):
┌──────────────────────────────────────────────────────────────┐
│  M (Modified): 仅本核心持有, 已被修改                        │
│  E (Exclusive): 仅本核心持有, 未修改                        │
│  S (Shared): 多个核心持有副本                                 │
│  I (Invalid): 已失效 (被其他核心修改后发送 RFO)               │
└──────────────────────────────────────────────────────────────┘

volatile 写:
  → 执行 RFO (Read For Ownership): 请求独占缓存行
  → 其他核心的同一缓存行 → 状态变为 I (Invalid)
  → 写入 M 状态

volatile 读:
  → 检查缓存行状态
  → 如果是 I → 必须从主存或其他核心重新读取
  → 读取到 S 或 E 状态

2.2 语义二:禁止指令重排序 (Ordering Prevention)

volatile 通过内存屏障 (Memory Barrier / Fence) 实现禁止重排序:

四种内存屏障:
┌─────────────────────────────────────────────────────────────────────┐
│                                                                      │
│  屏障类型        功能                        类比                      │
│  ────────        ──────                      ────                     │
│  LoadLoad       Load1 → [LoadLoad] → Load2   所有之前的读必须在       │
│                 阻止 Load1 与后续 Load 重排    后续读之前完成           │
│                                                                      │
│  StoreStore     Store1 → [StoreStore] → Store2 所有之前的写必须在       │
│                 阻止 Store1 与后续 Store 重排   后续写之前完成           │
│                                                                      │
│  LoadStore      Load1 → [LoadStore] → Store2  所有之前的读必须在       │
│                 阻止 Load1 与后续 Store 重排    后续写之前完成           │
│                                                                      │
│  StoreLoad      Store1 → [StoreLoad] → Load2  所有之前的写必须在       │
│                 阻止 Store1 与后续 Load 重排    后续读之前完成           │
│                 (最昂贵的屏障! 需要清空 Store Buffer)                   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

JMM 对 volatile 的屏障插入策略

volatile 写:
┌─────────────────────────────────────┐
│  [StoreStore]   ← 确保所有普通写在 volatile 写之前完成并可见        │
│  volatile 赋值                      │
│  [StoreLoad]    ← 最贵! 确保 volatile 写立即对后续读可见 (清空 SB)   │
└─────────────────────────────────────┘

volatile 读:
┌─────────────────────────────────────┐
│  volatile 读取                       │
│  [LoadLoad]    ← 确保 volatile 读完成后才执行后续普通读               │
│  [LoadStore]   ← 确保 volatile 读完成后才执行后续普通写               │
└─────────────────────────────────────┘

x86 下的实际实现 (简化)

x86 是 TSO (Total Store Order) 模型,天然保证 StoreStore、LoadLoad、LoadStore
→ 只有 StoreLoad 需要显式屏障

volatile 写在 x86 → 实际只生成 StoreLoad 屏障
                 → 具体指令: mfence 或 lock addl $0, -N(%rsp)

volatile 读在 x86 → 不需要任何屏障 (x86 TSO 已经保证)

2.3 volatile 不能保证原子性

这是面试最高频的陷阱题

volatile int count = 0;

// 两个线程同时执行:
count++;  // 这不是原子操作!

等价于:
int temp = count;   // 读 volatile (原子)
temp = temp + 1;    // 计算 (线程内)
count = temp;       // 写 volatile (原子)

// 线程交错:
Thread A: temp = count  →  0
Thread B: temp = count  →  0 (读到同样的值!)
Thread A: count = 0 + 1 → count = 1
Thread B: count = 0 + 1 → count = 1 (丢失一次递增!)
操作volatile 保证volatile 不保证
单次读 (int x = field)看到最新值
单次写 (field = x)其他线程立即看到
复合操作 (count++)单次读和写各自可见读-改-写 整体不是原子
long/double 非对齐可能读到"字撕裂" (Word Tearing)

正确做法

  • AtomicInteger (CAS 保证原子性 + volatile 保证可见性)
  • synchronized 包裹整个复合操作
  • ReentrantLock + 保护

2.4 为什么 DCL 需要 volatile —— 经典案例

这是 volatile 的必考题——DCL 构造函数的指令重排序问题:

问题: 没有 volatile 的 DCL
┌────────────────────────────────────────────────────────────────────┐
│ public class Singleton {                                           │
│     private static Singleton instance;  // 没有 volatile!           │
│                                                                     │
│     public static Singleton getInstance() {                         │
│         if (instance == null) {                                     │
│             synchronized (Singleton.class) {                        │
│                 if (instance == null) {                             │
│                     instance = new Singleton();  // 有问题的赋值!  │
│                 }                                                    │
│             }                                                        │
│         }                                                            │
│         return instance;                                             │
│     }                                                                │
│ }                                                                    │
└────────────────────────────────────────────────────────────────────┘

instance = new Singleton() 的三步 (Java 字节码):
  1. allocate    分配内存 (构造半初始化对象)
  2. invokespecial <init>  调用构造函数 (初始化字段)
  3. astore      赋值引用 (instance 指向内存)

C2 编译器/Core CPU 可以重排 2 和 3:
  
  正常顺序:    allocate → <init> → astore   (安全)
             实例:      [零值]    [Singleton{x=42}]
             
  重排后:     allocate → astore → <init>   (危险!)
             实例:      [零值]    [Singleton{x=0}]! 
             此时 instance != null, 但 x == 0!

线程交错:
  Thread A: allocate → astore (instance != null, 但 x=0!)
  Thread B: if (instance == null) → false → return instance
            → Thread B 看到 instance != null
            → 使用 instance.x → 读到 0 而不是 42!

修复: 加 volatile

private static volatile Singleton instance;
         ↑ 关键!

volatile 插入 StoreStore 屏障:
  allocate
  invokespecial <init>   // 构造函数
  [StoreStore]            ← 屏障: 确保构造函数完成前不赋值!
  astore instance         ← 赋值

→ 重排序被禁止! Thread B 看到 instance != null 时, 构造函数一定已完成

三、图解全景

3.1 volatile 写读的全流程

┌─────────────────────────────────────────────────────────────────────────┐
│                    volatile 写 → volatile 读 的全流程可见性                 │
│                                                                         │
│  Thread A (volatile 写)                  Thread B (volatile 读)           │
│  ┌────────────────────────────┐         ┌────────────────────────────┐  │
│  │                            │         │                            │  │
│  │ 普通变量 x = 1             │         │ 普通变量 r1 = x            │  │
│  │   ↓ (x 在 L1 缓存, 可能     │         │   ↑ (从 L1 缓存读, 还      │  │
│  │     其他核心看不到)          │         │     是旧值)                │  │
│  │                            │         │                            │  │
│  │ [StoreStore 屏障]          │         │ [LoadLoad 屏障]            │  │
│  │  (保证 x=1 已全局可见)      │         │  (保证 volatile 读完成后    │  │
│  │                            │         │   才读/写后续变量)          │  │
│  │ volatile flag = true       │         │ while(!flag){}  ← 读到    │  │
│  │  ↓ (MESI: 发送 RFO)       │         │   ↓ flag=true 后          │  │
│  │  ↓ (其他核心缓存行→Invalid) │         │   ↓ (从主存或 A 的缓存)     │  │
│  │                            │         │                            │  │
│  │ [StoreLoad 屏障]           │         │ 读 r1 = x → 一定是 1!     │  │
│  │  (清空 Store Buffer)       │         │  (因为 StoreStore 确保了    │  │
│  │                            │         │   x=1 在 flag=true 之前    │  │
│  │                            │         │  已可见)                    │  │
│  └────────────────────────────┘         └────────────────────────────┘  │
│                                                                         │
│ 可见性传递:                                                              │
│  x=1 ─[StoreStore]─→ flag=true ─[StoreLoad]─→ (其他核心看到)             │
│         ↑                              ↑                                │
│        普通写在 volatile 写之前        清空 Store Buffer 使写全局可见       │
│        必须完成                                                          │
└─────────────────────────────────────────────────────────────────────────┘

3.2 DCL 的修复对比

无 volatile (错误):
┌────────────────────────────────────────────────────────────────────┐
│ Thread A:           Thread B:                                       │
│                      │                                               │
│ allocate ──────┐    │ if (instance == null) → false                 │
│                │    │ return instance                               │
│ astore ────────┤──→ │ → instance 非空, 但构造函数还没执行完!        │
│                │    │ → instance.x 读到默认值 0                     │
│ <init> ────────┘    │                                               │
│ (构造函数太慢!)       │                                               │
└────────────────────────────────────────────────────────────────────┘

有 volatile (正确):
┌────────────────────────────────────────────────────────────────────┐
│ Thread A:           Thread B:                                       │
│                      │                                               │
│ allocate ────────── │ if (instance == null) → true (因为 StoreStore │
│ <init> ──────────── │  屏障阻止了引用赋值提前)                       │
│ [StoreStore barrier]│ → 等待 A 完成                                 │
│ astore ──────────── │ if (instance == null) → false (A 已完成)      │
│                      │ return instance → 完全初始化好的对象!         │
└────────────────────────────────────────────────────────────────────┘

3.3 四种屏障的 x86 映射

JMM 要求的屏障              x86 上的实际实现           是否有效
─────────────────          ────────────────          ────────
StoreStore                  无 (x86 TSO 天然保证)      no-op
LoadLoad                   无 (x86 TSO 天然保证)      no-op
LoadStore                  无 (x86 TSO 天然保证)      no-op
StoreLoad                  mfence / lock addl        显式屏障 (最贵!)

→ 在 x86 上, volatile 读是零开销的 (只保证缓存一致性)
→ 在 x86 上, volatile 写的开销就是 StoreLoad 屏障的开销 (约 20-50 cycles)
→ 在 ARM/PPC 等弱内存模型上, 四种屏障都需要! volatile 开销大得多

四、实战验证

验证 1: volatile 可见性

java
public class VolatileVisibility {
    static volatile boolean running = true;  // volatile → 可见!
    
    public static void main(String[] args) throws Exception {
        Thread worker = new Thread(() -> {
            long count = 0;
            while (running) {  // volatile 读 → 一定能看到主线程的修改
                count++;
            }
            System.out.println("Worker stopped, count=" + count);
        });
        worker.start();
        
        Thread.sleep(1000);
        running = false;  // volatile 写 → 立即对 worker 可见
        worker.join();
        
        // 输出: Worker stopped, count=...
        // 一定在 1-2 秒内退出 (vs 非 volatile 可能永远不退出)
    }
}

验证 2: volatile 不保证原子性

java
public class VolatileNotAtomic {
    static volatile int count = 0;
    static final int THREADS = 10;
    static final int INCREMENTS = 10000;
    
    public static void main(String[] args) throws Exception {
        Thread[] threads = new Thread[THREADS];
        
        for (int i = 0; i < THREADS; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < INCREMENTS; j++) {
                    count++;  // 不是原子的!
                }
            });
        }
        
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();
        
        System.out.println("Expected: " + (THREADS * INCREMENTS));
        System.out.println("Actual:   " + count);
        // 几乎每次都会丢失计数! 例如: Expected=100000, Actual=98734
    }
}

验证 3: DCL + volatile

java
public class Singleton {
    // 关键: volatile 禁止实例化时的指令重排序
    private static volatile Singleton instance;
    
    private final int value;
    
    private Singleton() {
        this.value = 42;
        // 模拟构造函数耗时操作
        try { Thread.sleep(1); } catch (InterruptedException e) {}
    }
    
    public static Singleton getInstance() {
        if (instance == null) {                    // 第一次检查 (无锁)
            synchronized (Singleton.class) {
                if (instance == null) {            // 第二次检查 (加锁)
                    instance = new Singleton();    // volatile 保证不会重排
                }
            }
        }
        return instance;
    }
    
    public int getValue() { return value; }
    
    // 测试: 多线程验证不出现半初始化对象
    public static void main(String[] args) throws Exception {
        for (int round = 0; round < 100; round++) {
            final int[] results = new int[1000];
            Thread[] threads = new Thread[1000];
            
            for (int i = 0; i < 1000; i++) {
                final int idx = i;
                threads[i] = new Thread(() -> {
                    results[idx] = Singleton.getInstance().getValue();
                });
            }
            
            for (Thread t : threads) t.start();
            for (Thread t : threads) t.join();
            
            // 验证所有线程都读到 42
            for (int r : results) {
                if (r != 42) {
                    System.out.println("ERROR! 读到 value=" + r);
                    return;
                }
            }
            // 重置 (为下一轮准备)
            instance = null;
        }
        System.out.println("All rounds passed: value=42 for all threads");
    }
}

验证 4: JOL 查看 volatile 字段

bash
# JOL 只能看到 volatile 字段的访问标志, 内存屏障需要通过 JIT 编译日志观察
# JIT 编译后查看是否插入 mfence 指令

# 使用 -XX:+PrintAssembly 查看 volatile 写的汇编
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly VolatileDemo

# 搜索 lock addl 或 mfence
# volatile 写附近应该出现:
#   lock addl $0x0,-0x40(%rsp)  ← x86 StoreLoad 屏障

五、面试视角

追问答案要点
volatile 的两层语义是什么?(1) 可见性:volatile 写立即刷入主存 + 无效化其他核心缓存行;volatile 读总是从主存获取最新值。(2) 禁止指令重排序:通过内存屏障阻止 volatile 操作与前后指令重排。
volatile 能保证原子性吗?不能。volatile 只保证单次读/写的原子性和可见性。复合操作(如 count++)包含读-改-写三步,这三步之间可能被其他线程打断。需要 AtomicInteger 或 synchronized。
什么是指令重排序?编译器和 CPU 为了优化指令流水线,在不改变单线程执行结果的前提下改变指令执行顺序。单线程安全,多线程下可能导致不可预期的结果。
volatile 与 synchronized 的区别?synchronized 保证可见性+原子性+互斥,但更重。volatile 只保证可见性+禁止重排序,不能保证原子性。volatile 没有锁竞争、没有线程阻塞。
DCL 为什么需要 volatile?instance = new Singleton() 不是原子操作——分配内存、调用构造函数、赋值引用三步可能被重排。没有 volatile 时,其他线程可能看到已赋值的引用但构造函数还没执行完(读到默认值)。volatile 的 StoreStore 屏障禁止构造函数在引用赋值之后执行。
volatile 的内存屏障在 x86 上怎么实现的?x86 TSO (Total Store Order) 模型天然提供 StoreStore、LoadLoad、LoadStore 保障。只有 StoreLoad 需要显式屏障——通过 mfencelock addl 指令实现。x86 上 volatile 读零额外指令开销。
volatile long/double 需要吗?在 32 位 JVM 上,long/double 的 64 位读写可能被分为两个 32 位操作(字撕裂)。64 位 JVM 上非 volatile 的 long/double 对齐到 8 字节时也是原子的。加上 volatile 确保不会发生字撕裂。
volatile 和 MESI 协议的关系?volatile 底层利用 MESI(或同类缓存一致性协议)的 RFO (Read For Ownership) 机制——volatile 写发出 RFO 请求独占缓存行,其他核心收到后将自己的缓存行标记为 Invalid,下次读取时必须从主存获取。
volatile 数组是怎么回事?volatile int[] arr 只保证 arr 引用本身的可见性,不保证数组元素的可见性。元素的 volatile 语义需要用 AtomicIntegerArrayUnsafeputIntVolatile

📚 相关链接

  • **JMM与happens-before** — volatile 变量规则是 8 条 hb 规则之一
  • **synchronized的JVM实现** — synchronized 的锁升级过程和 ObjectMonitor
  • **锁升级过程** — 偏向锁→轻量锁→重量锁的详细过程
  • **对象内存布局(MarkWord与Klass与实例数据)** — MarkWord 锁状态位
  • **锁消除与标量替换** — 锁消除使 synchronized 在不逃逸场景下被消除
  • ← 返回 **JMM索引**

Knowledge4J — Java 知识库