【JUC】内存屏障

内存屏障(Memory Barrier)详解

内存屏障是理解 volatile、synchronized、JMM 的底层关键。下面从为什么需要 、四种屏障 、volatile 如何插入屏障 、底层 CPU 如何实现四个层面展开。


一、为什么需要内存屏障

内存屏障要同时解决两类"乱序"问题:

1. 编译器/CPU 的指令重排序

编译器和 CPU 为了提升性能,在不改变单线程语义的前提下会重排指令。这在单线程没问题,但多线程下会破坏程序的预期。

2. 硬件层面的存储缓冲(Store Buffer)

现代 CPU 写数据不是直接写主存,而是先写入 Store Buffer,再异步刷回缓存/主存。这导致:

  • 写操作对其他 CPU 不是立即可见
  • 后续读操作可能越过尚未提交的写操作

内存屏障就是告诉 CPU/编译器:"屏障前后的内存操作,不许跨越这条线重排,且必须保证刷写/失效完成。"


二、四种基本内存屏障

JMM 定义了四种屏障,按"读写方向"组合:

屏障类型 指令示例 作用
LoadLoad Load1; LoadLoad; Load2 保证 Load1 的读先于 Load2 的读完成
StoreStore Store1; StoreStore; Store2 保证 Store1 的写先于 Store2 的写刷新到主存
LoadStore Load1; LoadStore; Store2 保证 Load1 的读先于 Store2 的写完成
StoreLoad Store1; StoreLoad; Load2 保证 Store1 的写先于 Load2 的读,且写对后续读可见

StoreLoad 是全能屏障,开销最大。它同时具备其他三种屏障的效果,因为要"清空 Store Buffer 并等待写可见",代价最高。

记忆方法:屏障名字 = "前面操作类型 + 后面操作类型",表示禁止这两类操作交换顺序。


三、JMM 的保守策略:volatile 如何插入屏障

JMM 对 volatile 采用一套保守但正确的屏障插入策略(JSR-133 规范):

1. volatile 写操作

复制代码
普通写...
   ↓
【StoreStore 屏障】   ← 保证之前的普通写都刷新到主存
   ↓
volatile 写
   ↓
【StoreLoad 屏障】    ← 保证 volatile 写先于后续任何读写(关键!)
   ↓
普通读/写...

2. volatile 读操作

复制代码
普通读...
   ↓
volatile 读
   ↓
【LoadLoad 屏障】     ← 保证 volatile 读先于后续普通读
   ↓
【LoadStore 屏障】    ← 保证 volatile 读先于后续普通写
   ↓
普通读/写...

用表格总结

操作 前插屏障 后插屏障
volatile 读 --- LoadLoad、LoadStore
volatile 写 StoreStore StoreLoad

注意 :这些屏障是保守策略,实际执行时编译器会根据上下文优化掉不必要的屏障(例如连续的 volatile 写之间可以合并屏障)。


四、用屏障解释两个经典场景

场景 1:DCL 单例为何需要 volatile

java 复制代码
instance = new Singleton();
// 实际三步:
// 1. memory = allocate()      分配内存
// 2. ctorInstance(memory)     初始化对象
// 3. instance = memory        赋值引用

若步骤 2、3 重排(1→3→2),另一个线程在第一次 if (instance == null) 检查时可能读到非 null 但未初始化的引用。

volatile 的作用 :写 volatile 前插入 StoreStore,保证"初始化对象"(普通写)先于"赋值引用"(volatile 写)完成,从而禁止 2、3 重排。

场景 2:状态标志的可见性

java 复制代码
// 线程 A
data = 42;          // 普通写
flag = true;        // volatile 写

// 线程 B
if (flag) {         // volatile 读
    print(data);    // 普通读
}

线程 A 侧 :volatile 写前有 StoreStore 屏障 → 保证 data = 42 先刷到主存,再写 flag。

线程 B 侧:volatile 读后有 LoadLoad 屏障 → 保证读到 flag=true 后,再去读 data 能读到 42。

这就是 volatile 实现 "安全发布" 的底层机制。


五、底层 CPU 如何实现(x86 为例)

JMM 的屏障是抽象规范,最终要落到具体 CPU 指令。不同架构实现不同:

x86 架构特点

  • 强内存模型:只有 StoreLoad 可能重排(因为 Store Buffer),LoadLoad/StoreStore/LoadStore 天然不乱序
  • 因此 x86 上:
    • LoadLoad、StoreStore、LoadStore 屏障 = 空操作(no-op),仅编译期约束
    • StoreLoad 屏障 = lock 前缀指令 或 mfence

lock 前缀指令做了什么

复制代码
lock addl $0, (%rsp)   // 常见的空操作加 lock 前缀

lock 前缀触发:

  1. 将当前 CPU 的 Store Buffer / 缓存行写回主存
  2. 基于 MESI 协议,使其他 CPU 中该地址的缓存行失效(Invalidate)
  3. 后续读操作必须重新从主存/缓存加载,从而看到最新值
  4. 同时充当全能内存屏障,禁止指令重排

不同架构对比

架构 内存模型 StoreLoad 实现 说明
x86 强(TSO) lock / mfence 只有 StoreLoad 需真实指令
ARM 弱 dmb ish 四种屏障都需显式指令
PowerPC 弱 sync / lwsync 屏障更重

这也是为什么在 ARM 上运行 Java(如手机、Apple M 系列)时,volatile 的开销比 x86 更大------弱内存模型需要更多显式屏障指令。


六、屏障与缓存一致性协议的关系

内存屏障不能孤立理解,它和 MESI 缓存一致性协议 配合工作:

复制代码
CPU0                     CPU1
─────                    ─────
Store x=1                Load x
  ↓ Store Buffer           ↓
【StoreLoad 屏障】        缓存行失效
  ↓ lock 指令             ↓
广播失效 x 的缓存行 ────→ 重新从主存加载 x=1
  • MESI 负责缓存行状态同步(Modified/Exclusive/Shared/Invalid)
  • 内存屏障 负责时序约束:保证失效广播完成、Store Buffer 清空后,才允许后续操作
  • 没有屏障,Store Buffer 中的写可能延迟可见,导致其他 CPU 读到旧值

七、屏障在 JVM 中的其他应用

内存屏障不只服务 volatile,JMM 中多处使用:

场景 屏障策略
volatile 读后 LoadLoad+LoadStore,写前 StoreStore,写后 StoreLoad
synchronized monitorenter 相当于读屏障,monitorexit 相当于写屏障(保证临界区内存可见)
final 字段 写 final 字段后插入 StoreStore,保证构造完成前不被发布
CAS/原子类 底层 lock cmpxchg,自带全能屏障效果
Thread.start() 启动线程前插入屏障,保证 start 前的操作对子线程可见

八、一张图总览 volatile 写的内存语义

复制代码
        线程A                        主内存                    线程B
          │                            │                        │
    普通写 data=42                    │                        │
          │                            │                        │
    ┌─────┴─────┐                      │                        │
    │ StoreStore│  保证 data 先刷出     │                        │
    └─────┬─────┘                      │                        │
          │──────── data=42 刷到主存 ──→│                        │
   volatile 写 flag=true               │                        │
          │──────── flag=true 刷到主存 ─→│                        │
    ┌─────┴─────┐                      │                        │
    │ StoreLoad │  保证 flag 写先完成   │                        │
    └─────┬─────┘                      │                        │
          │                            │      volatile 读 flag  │
          │                            │←──── 从主存读取 ────────┤
          │                            │                        │
          │                            │    ┌──────────────┐    │
          │                            │    │LoadLoad+Store│    │
          │                            │    │Load 屏障     │    │
          │                            │    └──────┬───────┘    │
          │                            │           │            │
          │                            │←── 普通读 data ─────────┤
          │                            │   读到 42(可见)        │

总结口诀

写前 StoreStore,写后 StoreLoad;读后 LoadLoad + LoadStore。

StoreLoad 最贵,是全屏障;x86 靠 lock,ARM 靠 dmb。

屏障管时序,MESI 管一致,二者配合才有可见性。

相关推荐
深入云栈1 小时前
Netty 4.2.x 源码深度解析 (二十三):ByteBuf 核心设计 —— 索引、类型与零拷贝
java·后端·架构
爱笑的k111 小时前
nano-vllm 源码剖析:千行 Python 手写离线推理引擎的调度、显存与并行时序
开发语言·python·vllm
wang_shu_mo_ran2 小时前
MyBatis 从入门到进阶(一)
java·spring·mybatis
X1A0RAN2 小时前
PPT 图片批量处理教程:导出全部幻灯片 · 提取全部素材
开发语言·javascript·powerpoint
量化分析码农3 小时前
股票历史数据的复权参数对照:多级别返回的字段与数值差异
java·javascript·后端
猫猫不是喵喵.3 小时前
从0到1创建一个微服务项目
java·数据库·微服务·1024程序员节
熊猫钓鱼>_>4 小时前
普通用户视角的端侧智能体验手记【鸿蒙心迹】
开发语言·人工智能·华为·智能手机·性能优化·ai编程·harmonyos
瑞雪兆丰年兮4 小时前
JavaWeb-day13-SpringBootWeb事务管理&AOP
java·开发语言·springboot·javaweb·事务·web·aop
曲径海盗4 小时前
Try-Except怎么用,3个追问解答在自动化下的应用
java·jvm·oracle
Cosolar5 小时前
极简智库:快速搭建你的开源项目教程阅读平台,让 docs/ 目录被更多人看见
java·架构·github