volatile实现可见性和禁止指令重排的原理

62 阅读5分钟

Java Volatile 关键字底层原理解析

一、先通俗理解核心概念

在讲原理前,先明确两个核心问题的本质:

  • 可见性:多线程环境下,一个线程修改了共享变量,其他线程能立刻看到这个修改后的值(而非缓存中的旧值)。
  • 指令重排:CPU/编译器为了优化执行效率,会在不改变单线程语义的前提下,调整指令的执行顺序,可能导致多线程下逻辑错乱。

二、volatile 实现可见性的原理

volatile 实现可见性的核心是内存屏障(Memory Barrier) + 缓存一致性协议

1. 底层硬件基础:CPU 缓存与 MESI 协议

现代 CPU 为了提升效率,会给每个核心配备独立的高速缓存(L1/L2/L3),共享变量会先加载到缓存中操作,再同步到主存。这就导致:线程 A 修改了缓存中的变量,还没同步到主存时,线程 B 读取的是自己缓存中的旧值,这就是可见性问题

CPU 层面通过 MESI 缓存一致性协议 解决这个问题(主流 x86 架构都支持):

  • 当一个 CPU 核心修改了缓存中共享变量的状态(比如从「共享」改为「独占」),会通过总线广播这个修改;
  • 其他核心收到广播后,会将自己缓存中该变量的副本标记为「失效」;
  • 当其他核心需要读取这个变量时,发现缓存失效,就会从主存重新加载最新值,而非使用缓存中的旧值。
2. volatile 对可见性的增强

Java 中的 volatile 关键字,会通过 JVM 向 CPU 发送内存屏障指令,强制触发以下行为:

  1. volatile 变量时:

    1. 立即将该变量在当前线程工作内存(对应 CPU 缓存)中的值刷新到主存
    2. 同时使其他线程缓存中该变量的副本失效。
  2. volatile 变量时:

    1. 直接从主存读取最新值,而非线程的工作内存(缓存)。

简单说:volatile 禁止了变量在工作内存中的「缓存」,强制读写都走主存,从而保证可见性。

三、volatile 禁止指令重排的原理

volatile 禁止指令重排的核心是 内存屏障(Memory Barrier/Fence) ,JVM 会在 volatile 变量的读写操作前后插入特定的内存屏障,约束指令重排的范围。

1. 内存屏障的类型(x86 架构核心)
屏障类型作用
LoadLoad禁止上面的读操作和下面的读操作重排
StoreStore禁止上面的写操作和下面的写操作重排
LoadStore禁止上面的读操作和下面的写操作重排
StoreLoad禁止上面的写操作和下面的读/写操作重排(最严格,x86 主要依赖这个)
2. volatile 变量的内存屏障插入规则

JVM 对 volatile 变量的读写会插入以下屏障(以 x86 为例):

  • 写 volatile 变量后:插入 StoreStore + StoreLoad 屏障

    • StoreStore:确保普通变量的写操作先于 volatile 变量的写操作刷入主存;
    • StoreLoad:确保 volatile 变量的写操作完成后,再执行后续的任何读写操作。
  • 读 volatile 变量前:插入 LoadLoad + LoadStore 屏障

    • LoadLoad:确保 volatile 变量的读操作先于后续普通变量的读操作;
    • LoadStore:确保 volatile 变量的读操作先于后续普通变量的写操作。
3. 通俗解释

内存屏障就像「指令执行的栅栏」:

  • 栅栏前的指令必须全部执行完,才能执行栅栏后的指令;
  • 栅栏后的指令也不能提前到栅栏前执行。

volatile 通过插入这些栅栏,阻止了编译器/CPU 将 volatile 变量的读写指令与其他指令重排,从而保证了指令执行顺序的「有序性」。

四、补充:volatile 不保证原子性

需要特别注意:volatile 只能保证可见性和有序性,不能保证原子性(比如 count++ 这种复合操作,依然需要加锁或用原子类)。

示例:volatile 变量的有序性验证

public class VolatileReorderDemo {
    private static volatile boolean flag = false;
    private static int a = 0;

    public static void main(String[] args) throws InterruptedException {
        // 线程 1:修改 flag 前先给 a 赋值
        Thread t1 = new Thread(() -> {
            a = 1;          // 普通写
            flag = true;    // volatile 写,插入 StoreStore + StoreLoad 屏障
        });

        // 线程 2:读取 flag 后再读 a
        Thread t2 = new Thread(() -> {
            if (flag) {     // volatile 读,插入 LoadLoad + LoadStore 屏障
                System.out.println(a); // 必然输出 1,不会因为重排输出 0
            }
        });

        t2.start();
        Thread.sleep(100);
        t1.start();
    }
}

如果没有 volatile,编译器可能会将 a=1flag=true 重排,导致 t2 看到 flag=truea=0;加了 volatile 后,指令重排被禁止,t2 看到 flag=true 时,a=1 一定已经执行完成。

总结

  1. 可见性原理volatile 通过内存屏障强制变量读写走主存,并借助 CPU 的 MESI 缓存一致性协议,让其他线程及时看到变量的最新值。
  2. 禁止指令重排原理volatile 通过在变量读写前后插入特定的内存屏障(栅栏),约束编译器/CPU 的指令重排行为,保证指令执行顺序的有序性。
  3. 核心底层支撑:CPU 层面的内存屏障指令 + MESI 缓存一致性协议,JVM 层面将 volatile 语义映射为这些硬件指令。