【JUC】四种内存屏障示例

四种内存屏障的代码示例详解

下面针对 LoadLoad、StoreStore、LoadStore、StoreLoad 四种屏障,各用一个可复现的经典例子,结合代码和内存图说明"如果没有屏障会发生什么"以及"屏障如何修复"。

⚠️ 说明:内存屏障是 JMM 的抽象概念,Java 源码中没有直接写屏障的语法 。下面用 volatile (JMM 自动插入屏障)或 Unsafe/原子类(显式屏障)来触发,并解释屏障在其中起的作用。


一、LoadLoad 屏障

语义

保证前一个读 先于后一个读完成,禁止两个读操作重排。

关键点

LoadLoad 主要防止读-读重排 ,但单看"两个读"很难观察出错。真正有意义的场景是:volatile 读之后,普通读不能被重排到 volatile 读之前,否则会读到过期数据。

代码示例

java 复制代码
public class LoadLoadDemo {
    static int data = 0;              // 普通变量
    static volatile boolean flag = false; // volatile 变量

    // 线程 A:写数据
    static void writer() {
        data = 42;
        flag = true;                  // volatile 写
    }

    // 线程 B:读数据
    static void reader() {
        if (flag) {                   // volatile 读
            // ↓↓↓ 这里就是 LoadLoad 屏障的位置 ↓↓↓
            // 保证"读 flag"先于"读 data",否则 data 可能读到 0
            int d = data;             // 普通读
            System.out.println("data = " + d); // 保证是 42
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(LoadLoadDemo::writer);
        Thread t2 = new Thread(LoadLoadDemo::reader);
        t2.start();
        t1.start();
        t1.join();
        t2.join();
    }
}

如果没有 LoadLoad 屏障会怎样

复制代码
线程B的执行顺序被重排:
  1. 读 data   ← 先读 data(此时 data 还是 0)
  2. 读 flag   ← 再读 flag(为 true)
  3. 判断 flag=true,打印 data=0  ← 错误!

LoadLoad 屏障保证 :flag 读操作之后,data 的读操作不能被提到 flag 读之前,从而保证读到 flag=true 时,data 一定已是最新值 42。

屏障位置图

复制代码
   volatile 读 flag        ←──────────┐
        │                            │
   ┌────┴─────┐                      │  LoadLoad 屏障
   │ LoadLoad │  禁止下面的读上移     │  (JMM 在读后插入)
   └────┬─────┘                      │
        │                            │
   普通读 data                       │

二、StoreStore 屏障

语义

保证前一个写 先于后一个写刷新到主存,禁止两个写操作重排。

经典场景:DCL 单例 / 对象安全发布

java 复制代码
public class StoreStoreDemo {
    private static volatile StoreStoreDemo instance;
    private int value = 100;   // 普通字段

    private StoreStoreDemo() {
        value = 100;
    }

    public static StoreStoreDemo getInstance() {
        if (instance == null) {
            synchronized (StoreStoreDemo.class) {
                if (instance == null) {
                    // ↓↓↓ 这里展开就是 StoreStore 屏障 ↓↓↓
                    instance = new StoreStoreDemo();
                }
            }
        }
        return instance;
    }
}

展开 instance = new StoreStoreDemo() 的三步

java 复制代码
memory = allocate();          // ① 分配内存
ctorInstance(memory);         // ② 初始化对象(写 value=100 等)
instance = memory;            // ③ volatile 写,赋值引用

如果没有 StoreStore 屏障

编译器/CPU 可能重排成 ① → ③ → ②:

复制代码
线程A: 分配内存 → instance 指向内存 → 初始化对象
                                    ↑
线程B: 在此时检查 instance != null,
       拿到未初始化完成的对象,读到 value=0  ← 错误!

StoreStore 屏障保证 :volatile 写 instance 之前插入屏障,使步骤②(普通写 value)必须先于步骤③(volatile 写 instance)完成并刷新到主存。

屏障位置图

复制代码
   普通写 value = 100
        │
   ┌────┴──────┐
   │ StoreStore│  禁止下面的写提前
   └────┬──────┘
        │
   volatile 写 instance   ← 保证 value 已刷新到主存

三、LoadStore 屏障

语义

保证前一个读 先于后一个写完成,禁止"读-写"重排。

代码示例:一个标志位控制的循环

java 复制代码
public class LoadStoreDemo {
    static volatile boolean running = true;
    static int counter = 0;

    static void worker() {
        while (running) {          // volatile 读
            // ↓↓↓ 这里就是 LoadStore 屏障的位置 ↓↓↓
            counter++;             // 普通写
        }
        System.out.println("退出,counter = " + counter);
    }

    static void stopper() throws InterruptedException {
        Thread.sleep(100);
        running = false;           // volatile 写
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(LoadStoreDemo::worker);
        t.start();
        stopper();
        t.join();
    }
}

如果没有 LoadStore 屏障

注意 counter++ 本身不是原子操作,但这里我们只关注屏障对"读 running"和"写 counter"的顺序约束。

设想编译器把 counter++(写)提前 到 running 读之前:

复制代码
错误重排:
  1. counter++      ← 写操作提前
  2. 读 running
  ...

更典型的真实危害是:while (running) 读之后,紧随的普通写如果被重排到读之前,会导致程序在"检查条件"之前就修改了状态,破坏逻辑依赖。

LoadStore 屏障保证 :volatile 读 running 之后,counter++ 的写操作不能被提到 running 读之前,保证逻辑上的"先检查后操作"顺序。

屏障位置图

复制代码
   volatile 读 running
        │
   ┌────┴─────┐
   │LoadStore │  禁止下面的写上移
   └────┬─────┘
        │
   普通写 counter++

四、StoreLoad 屏障(最重要 / 开销最大)

语义

保证前一个写 先于后一个读 完成,且该写对后续读可见 。它是全能屏障,同时具备其他三种效果。

关键点

StoreLoad 是唯一在 x86 上需要真实指令 (lock 前缀 / mfence)的屏障。它要清空 Store Buffer,代价最大。

经典场景:用 volatile 实现自旋锁 / 安全发布

java 复制代码
public class StoreLoadDemo {
    static int data = 0;
    static volatile boolean ready = false;

    // 线程 A:发布数据
    static void producer() {
        data = 42;          // 普通写
        ready = true;       // volatile 写
        // ↓↓↓ volatile 写后插入 StoreLoad 屏障 ↓↓↓
        // 保证 ready=true 已刷到主存,后续任何读都能看到
    }

    // 线程 B:消费数据
    static void consumer() {
        while (!ready) {    // volatile 读,自旋
            // 空转
        }
        // ↓ volatile 读后有 LoadLoad/LoadStore 屏障 ↓
        System.out.println("data = " + data); // 保证是 42
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(StoreLoadDemo::producer);
        Thread t2 = new Thread(StoreLoadDemo::consumer);
        t2.start();
        t1.start();
        t1.join();
        t2.join();
    }
}

如果没有 StoreLoad 屏障

设想 ready = true(volatile 写)之后紧跟一个普通读:

复制代码
线程A:
  1. data = 42
  2. ready = true       ← 写进 Store Buffer,尚未刷到主存
  3. 读某个普通变量 x    ← 没有 StoreLoad 屏障,这个读可能先于 ready 的刷新完成

线程B:
  看到 ready=false(因为 A 的写还在 Store Buffer 里)

StoreLoad 屏障保证 :ready = true 必须真正刷到主存(并让其他 CPU 缓存失效)之后 ,才允许后续的读写操作,从而保证线程 B 一旦看到 ready=true,data=42 也一定可见。

屏障位置图(x86 上的 lock 指令)

复制代码
   普通写 data = 42
        │
   volatile 写 ready = true
        │
   ┌────┴──────┐
   │ StoreLoad │   ← x86: lock addl $0,(%rsp) / mfence
   │           │     清空 Store Buffer + 广播缓存失效
   └────┬──────┘
        │
   后续读/写
        │
   ─────┴────→ 其他 CPU 此时才能读到 ready=true

五、四者对比总表

屏障 禁止的重排 典型触发场景 无屏障的危害 x86 实现
LoadLoad 读 → 读 volatile 读后读普通变量 读到过期数据 空操作
StoreStore 写 → 写 volatile 写前写普通变量(DCL) 对象未初始化就被发布 空操作
LoadStore 读 → 写 volatile 读后写普通变量 逻辑顺序被破坏 空操作
StoreLoad 写 → 读 volatile 写后读任意变量 写不可见,读到旧值 lock / mfence

六、补充:如何"看到"这些屏障

如果你想实证观察,可以:

1. 用 javap -v 看字节码

volatile 变量在字节码中有 ACC_VOLATILE 标志:

bash 复制代码
javap -v LoadLoadDemo.class | grep -A2 "flag"
# 会看到 flags: ACC_STATIC, ACC_VOLATILE

2. 用 JIT 汇编日志看 lock 指令

bash 复制代码
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly LoadLoadDemo

在 x86 上,volatile 写附近能看到 lock addl $0x0,(%rsp) 或 mfence。

3. 用 Unsafe 显式触发屏障(JDK9+ 用 VarHandle)

java 复制代码
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

public class ExplicitBarrier {
    static int x = 0, y = 0;

    public static void main(String[] args) {
        VarHandle.fullFence();   // StoreLoad 全屏障
        VarHandle.acquireFence(); // 类似 LoadLoad + LoadStore
        VarHandle.releaseFence(); // 类似 StoreStore + LoadLoad
    }
}

VarHandle 的 fullFence/acquireFence/releaseFence 是 Java 层最接近"显式内存屏障"的 API。


总结

复制代码
LoadLoad   : 读后读  → volatile 读之后,普通读不能上移
StoreStore : 写前写  → volatile 写之前,普通写必须先完成
LoadStore  : 读后写  → volatile 读之后,普通写不能上移
StoreLoad  : 写后读  → volatile 写之后,写必须可见才能继续读

一句话记忆 :屏障名字 = "前操作 + 后操作",禁止这两类操作交换顺序;其中 StoreLoad 是全能屏障,也是性能分水岭。


附带其他重要笔记结论

相关推荐
界首大学1 小时前
CUDA C++ 高效入门第四章 -- CUDA 同步技术之线程块同步
1024程序员节·cuda c++
宇明一不急1 小时前
numa拓扑感知调度
1024程序员节
Stitch .1 小时前
投稿正文_图床版
1024程序员节
stereohomology1 小时前
更新字帖发现zcode的功能很丰富:dynamic workflows的表现
1024程序员节
张小姐的猫1 小时前
【MySQL】内置函数
1024程序员节
乔旭伟1 小时前
Vben3 中后台表格封装实战:useVbenVxeGrid 的全局收敛与七个坑
前端·anti-design-vue·1024程序员节
Liekkas Kono1 小时前
20261017-20261024 周刊
1024程序员节·swhl
Daneas1 小时前
3060 12G显卡+16G内存可以跑的Z-image效果与SD对比
ai作画·1024程序员节
我不会插花弄玉1 小时前
12. 视图【由浅入深-MySQL】
1024程序员节