Java 内存模型(JMM)- happens-before 与内存屏障

Java 内存模型(JMM)的 happens-before 规则与这些屏障有什么关系?

下面先解释 happens-before 的抽象概念,再说明它如何通过内存屏障落地实现。


一、happens-before 是什么?

happens-before 是 JMM 定义的一种偏序关系。如果两个操作满足 happens-before 关系,那么第一个操作的结果对第二个操作可见 ,并且第一个操作的执行顺序必须在第二个操作之前(不会被重排序)。

  • hb(A, B) 表示:A happens-before B。
  • 我们编程时借助 volatilesynchronizedThread.start() 等来建立 happens-before 关系,从而保证线程安全。

关于 happens-before 的精确定义,可以参考 Java 语言规范(JLS §17.4.5) 中的表述:

两个操作可以用 happens-before 关系来排序。如果一个操作 happens-before 另一个操作,那么第一个操作的结果对第二个操作可见,并且第一个操作的执行顺序在第二个操作之前。

更严格的形式化定义:

  1. 偏序关系 :happens-before 是程序执行中操作之间的一种偏序关系(自反、传递、反对称)。

  2. 满足的条件 :对于任意两个操作 A 和 B,如果满足以下任一条件,则称 A happens-before B

    • 程序次序:A 和 B 在同一个线程内,且 A 在代码顺序上出现在 B 之前。
    • Volatile 规则 :A 是对 volatile 变量的写,B 是对同一个 volatile 变量的读,且 B 在时间上后续看到该写。
    • 锁规则:A 是对监视器锁的解锁,B 是对同一个锁的加锁。
    • 线程启动 :A 是 Thread.start(),B 是新线程中的任意操作。
    • 线程终止 :A 是线程中的任意操作,B 是其他线程上对该线程的 Thread.join() 成功返回。
    • 中断 :A 是 Thread.interrupt(),B 是被中断线程检测到中断。
    • 传递性:若 A happens-before B 且 B happens-before C,则 A happens-before C。
    • 终结规则 :A 是对象的构造器的结束,B 是该对象的 finalize() 方法的开始。
  3. 重要特性:如果两个操作没有 happens-before 关系,JVM 允许对它们任意重排序,并且不保证可见性(即可能出现数据竞争)。

一句话定义

happens-before 是 Java 内存模型中用于保证内存可见性操作顺序的偏序关系;当 A happens-before B 时,A 对所有后续操作(包括 B)的效果都是确定的、可见的,且不会被重排序到 B 之后。


二、与 volatile 相关的 happens-before 规则

JMM 对 volatile 定义了如下规则:

  • 写规则 :对 volatile 变量 v 的写,happens-before 于后续(按时间顺序)对同一变量 v 的读。
  • 传递性:如果 A happens-before B,B happens-before C,则 A happens-before C。

这个规则正是通过内存屏障来实现的。我们看一个典型例子:

java 复制代码
// 线程 A
x = 42;           // 普通写
volatile v = 1;   // volatile 写

// 线程 B
if (v == 1) {     // volatile 读
    y = x;        // 普通读
}

根据规则:x = 42 在时间上先于 v = 1(假设线程 A 先执行),而且 v = 1v == 1 满足 volatile 变量规则,因此有 happens-before 链条:

x = 42 → (程序顺序) → v = 1 → (volatile 规则) → v == 1 → (程序顺序) → y = x

所以最终 x = 42 happens-before y = x,线程 B 读取 x 时会看到 42(而不是旧值或未初始化值)。


三、内存屏障如何实现 volatile happens-before

1. 实现写规则(volatile 写 happens-before volatile 读)

  • volatile 写前插入 StoreStore :确保 x = 42 不会被重排到 v = 1 之后。如果发生重排,线程 B 可能在看到 v == 1x 还未更新,破坏 happens-before。
  • volatile 写后插入 StoreLoad :强制将写缓冲刷入主存,并使其他 CPU 的缓存行失效。这样后续线程 B 读 v 时一定从主存拿到最新值。
  • volatile 读后插入 LoadLoad + LoadStore :确保 v == 1 的读不会与后续普通读/写重排,保证看到 volatile 读的结果后,后续操作能看到之前所有写入。

2. 实现传递性

传递性靠编译器和 CPU 遵守这些屏障规则,禁止可能导致违反 happens-before 的重排序。


四、happens-before 全规则概览

除了 volatile,还有以下常见规则(每条规则背后也依赖屏障或锁实现):

规则 示例 底层实现手段
程序顺序规则 同一线程内,前面的操作 happens-before 后面的操作 编译器不重排(依赖依赖关系)
volatile 规则 对 volatile 写 happens-before 后续对同一变量的读 内存屏障(StoreStore/StoreLoad/LoadLoad/LoadStore)
锁规则 解锁 happens-before 后续的加锁 释放锁时 StoreLoad(类似 volatile 写),获取锁时类似 volatile 读
线程启动规则 Thread.start() happens-before 该线程内的任何操作 依赖于内存同步(start() 内部有同步)
线程终止规则 线程内的任何操作 happens-before Thread.join() 返回 同样依赖同步
中断规则 调用 interrupt() happens-before 被中断线程检测到中断 通过 volatile 变量实现中断标志
finalizer 规则 构造函数结束 happens-before finalizer 开始 特殊的内存屏障(防止构造函数内对象逃逸)

五、为什么 volatile 的 happens-before 规则不保证原子性?

happens-before 只定义了顺序和可见性 ,但并不限制一个操作(如读-改-写)的内部步骤不被其他线程交错。例如 count++ 可以分为三步,线程 A 读 count 和线程 B 读 count 可以同时发生(虽然两者都要先于后续写,但两者之间没有 happens-before 关系),导致丢失更新。因此原子性需要额外的锁或 CAS。


六、下一层:Java 代码到汇编的真实屏障指令

我们在 Java 里写的 volatile 最终变成了什么 CPU 指令?以 x86_64 为例:

java 复制代码
volatile int v = 42;

对应的汇编(通过 JVM 的 JIT 编译后)类似:

asm 复制代码
mov     dword ptr [rsp+...], 42   ; 普通写
lock add dword ptr [rsp], 0       ; lock 前缀指令(既是 StoreLoad 屏障,也充当 StoreStore 屏障,因为 x86 不重排写-写)

lock addl $0x0, (%rsp) 就是 x86 上的全功能屏障,实现了 StoreStore + StoreLoad。

volatile 读在 x86 上通常就是普通 mov 指令,因为 x86 的读操作不会被重排,但仍会配合缓存一致性协议从主存读取最新值(因为其他 CPU 的写可能通过 RFO 使缓存行无效)。


七、总结

概念 抽象层面 实现层面
happens-before JMM 定义的可见性与顺序规则 -
volatile 写规则 对 volatile 写 happens-before 后续读 StoreStore + StoreLoad 屏障
volatile 读规则 - LoadLoad + LoadStore 屏障
最终保证 跨线程的可见性和禁止特定重排序 具体 CPU 指令(x86: lock 前缀) + 缓存一致性协议(MESI)
相关推荐
plainGeekDev8 小时前
Android Framework 面试题:Binder都说不清楚,简历别写精通了
android·java
Gauss松鼠会8 小时前
【GaussDB】基于SpringBoot实现操作GaussDB(DWS)的项目实战
java·数据库·经验分享·spring boot·后端·sql·gaussdb
Gauss松鼠会8 小时前
【GaussDB】GaussDB 常见问题及解决方案汇总
java·数据库·算法·性能优化·gaussdb·经验总结
xiaogg36789 小时前
k8s 部署yaml文件和Dockerfile文件配置
java·docker·kubernetes
砍材农夫9 小时前
物联网 基于netty构建mqtt协议规范(发布/订阅模式)
java·开发语言·物联网·netty
techdashen9 小时前
Rust 泛型 vs Java 泛型:它们看起来相似,但骨子里截然不同
java·开发语言·rust
人道领域9 小时前
【LeetCode刷题日记】106.从遍历序列重建二叉树:手撕递归边界,彻底搞懂左闭右闭 vs 左闭右开
java·算法·leetcode
luck_bor9 小时前
Map&Stream流
java·开发语言