欢迎光临
我们一直在努力

详解 volatile 的内存屏障:它是如何禁止指令重排并保证可见性的

什么是 volatile?

volatile 是并发编程中一个关键的修饰符,它保证了对共享变量操作的两大特性:可见性(Visibility)有序性(Ordering)

synchronized 锁机制不同,volatile 是一种轻量级的同步机制,它不会引起线程上下文切换和线程调度。但要深入理解它的工作原理,就必须了解底层是如何通过内存屏障(Memory Barrier / Memory Fence) 来实现的。

CPU与指令重排的挑战

为了最大化性能,现代编译器和CPU通常会对指令进行重新排序(Instruction Reordering),前提是这种重排不会改变单线程的执行结果(即遵循“as-if-serial”语义)。

然而,在多线程环境中,指令重排可能导致意料之外的错误,例如一个线程读取到了另一个线程尚未完成写入的中间状态数据。

volatile 与内存屏障的对应关系(以 Java 为例)

Java内存模型(JMM)规定,当对一个 volatile 变量进行读写操作时,必须在汇编级别插入特定的内存屏障,以强制遵守特定的执行顺序和数据同步。

JMM主要定义了四种内存屏障:LoadLoad, StoreStore, LoadStore, StoreLoad。

1. volatile 写操作(Store)

当执行 volatile 变量的写入操作时,JMM会做两件事情:

  1. 数据刷入主内存: 强制将当前CPU缓存中的新值立即写入主内存。
  2. 插入屏障:volatile 写操作之后插入一个 StoreLoad 屏障。

屏障效果:

  • StoreStore 屏障 (在写操作之前): 确保 volatile 写操作之前的普通写操作不会被重排到 volatile 写操作之后。
  • StoreLoad 屏障 (在写操作之后): 这是一个全能屏障,它确保所有的写操作(包括 volatile 自身的写)都对其他处理器可见,并且禁止任何后续的读操作或写操作被重排到 volatile 写操作之前。
// volatile 写操作后的内存屏障效果
private int a; // 普通变量
private volatile boolean flag = false;

// 线程A 执行:
void write() {
    a = 10;     // 1. 普通写操作
    // --> StoreStore 屏障确保 a=10 先于 flag=true 执行
    flag = true;  // 2. volatile 写操作(写入主内存,并插入 StoreLoad 屏障)
    // --> StoreLoad 屏障:保证 a=10 和 flag=true 对所有线程可见
    // 之后的操作不会被重排到 flag=true 之前。
}

2. volatile 读操作(Load)

当执行 volatile 变量的读取操作时,JMM也会插入屏障:

  1. 缓存失效: 强制使当前CPU工作内存中的缓存值失效,必须从主内存中重新加载最新值。
  2. 插入屏障:volatile 读操作之前和之后插入屏障。

屏障效果:

  • LoadLoad 屏障 (在读操作之后): 确保 volatile 读操作之后的所有普通读操作不会被重排到 volatile 读操作之前。
  • LoadStore 屏障 (在读操作之后): 确保 volatile 读操作之后的所有普通写操作不会被重排到 volatile 读操作之前。
// volatile 读操作后的内存屏障效果
private int b; // 普通变量
private volatile boolean flag = false;

// 线程B 执行:
void read() {
    // --> LoadLoad/LoadStore 屏障确保后续操作不会被重排到 flag 读操作之前
    if (flag) { // 1. volatile 读操作(强制从主内存读取)
        // 2. 普通读操作
        b = a; 
    }
}

总结:如何实现可见性与禁止重排

1. 保证可见性(Visibility)

可见性是通过 volatile 写操作后的 StoreLoad 屏障 实现的。当一个线程写入 volatile 变量时,StoreLoad 屏障触发一个特殊操作:它将本地缓存数据写回主内存,并向其他CPU发送信号(通常是MESI协议中的Invalidate消息),强制其他线程的本地缓存行失效。这样,其他线程在下次读取该变量时,就必须从主内存重新加载,从而保证了数据的“新鲜”状态。

2. 禁止指令重排(Ordering)

通过在读写操作周围部署的 LoadLoad, LoadStore, StoreStore, StoreLoad 屏障,确保了以下四种重排是被禁止的:

操作顺序 是否允许重排? 屏障类型 目的
1. 普通读/写 -> volatile 读 禁止 LoadLoad/LoadStore 确保先写操作的结果对 volatile 读可见
2. volatile 读 -> 普通读/写 禁止 LoadLoad/LoadStore 确保 volatile 读之后的操作不会被提前
3. 普通写 -> volatile 写 禁止 StoreStore 确保 volatile 写之前的写操作先于 volatile 写完成
4. volatile 写 -> 普通读/写 禁止 StoreLoad 确保 volatile 写及其同步效果先于后续操作完成

正是这些内存屏障,在底层硬件和操作系统层面提供了严格的顺序保证,使得 volatile 成为多线程编程中实现状态标志或简单数据共享的可靠工具。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » 详解 volatile 的内存屏障:它是如何禁止指令重排并保证可见性的
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址