目录

一、什么是JMM

二、并发三大核心特性(并发问题根源)

1. 可见性

概念

底层硬件根源

核心结论

2. 原子性

概念

底层问题

3. 有序性

概念

两类重排序(遵循 as‑if‑serial 规则)

补充

三、内存屏障(JMM 底层核心)

核心规则

四类基础内存屏障

StoreLoad 全能屏障(重中之重)

关键误区

x86 与 ARM 硬件差异

四、Volatile 关键字底层原理

核心能力总结

1. 可见性实现原理(读写双向机制)

Volatile 写操作(写端)

Volatile 读操作(读端)

2. 有序性实现原理

3. 为什么 Volatile 不保证原子性

五、Happens‑Before 先行发生原则

核心定义

高频六大核心规则

JMM 完整层级链路

六、Final 关键字 JMM 并发语义

核心作用

底层原理

Final 唯一致命漏洞:构造器泄露 this 引用

漏洞演示代码

常见 this 泄露业务场景

开发强制规范

七、综合实战:DCL 双重检查锁(JMM 全家桶案例)

线程安全 DCL 单例完整代码

new 对象底层四步拆解

无 volatile 的安全隐患

Volatile 解决 DCL 问题原理

Happens‑Before 推导 DCL 正确性

最终结论

八、JMM 终极总结


一、什么是JMM

JMM 全称 Java Memory Model(Java 内存模型),是 JVM 官方定义的一套抽象内存规范、并发内存语义标准,并非真实的内存结构。

JMM 的核心作用:屏蔽不同硬件(x86、ARM)、不同操作系统的内存差异,统一规范 Java 多线程的内存行为,专门解决多线程并发编程的三大核心问题:可见性、原子性、有序性。

核心前提: 单线程环境下,JMM 的所有问题都不会暴露,程序执行完全符合代码逻辑。所有 Java 并发 Bug,本质都是多线程共享变量交互引发的,全部由 JMM 三大特性缺失导致。

二、并发三大核心特性(并发问题根源)

1. 可见性

概念

多个线程共享同一个变量,一个线程修改了共享变量的值,其他线程无法立刻感知到最新修改值,始终读取本地旧缓存数据,导致数据不一致。

底层硬件根源
  1. CPU 多级缓存架构:每个 CPU 核心拥有私有 L1、L2 缓存,多核共享 L3 缓存与主内存。线程优先操作 CPU 私有缓存,多核缓存相互隔离,数据无法实时同步。
  2. MESI 缓存一致性协议:硬件层面保证多核缓存最终一致性,但为了极致性能,CPU 引入两大异步缓冲区:
  • Store‑Buffer(写缓冲区):CPU 写数据先存入缓冲区,无需等待其他核心应答,直接执行后续指令
  • Invalidate‑Queue(失效队列):接收其他核心的缓存失效通知,先暂存队列,延后执行缓存作废操作
核心结论

Store‑Buffer 和 Invalidate‑Queue 的异步延迟特性,是多线程可见性问题的根本原因。

2. 原子性

概念

一组操作不可被线程切换、不可被中断,要么全部执行成功,要么完全不执行,不存在中间状态。

底层问题

Java 中很多看似单一的代码,会被拆解为多条 CPU 指令,典型案例:i++ 指令拆解:读取主内存数据 → 寄存器自增运算 → 写回主内存

三条指令执行过程中,可随时发生线程切换,其他线程会读取到操作中间值,彻底破坏数据一致性。

3. 有序性

概念

代码的书写逻辑顺序,与 JVM、CPU 实际执行顺序不一致,指令发生重排序,多线程场景下引发诡异并发 Bug(典型:DCL 单例半初始化问题)。

两类重排序(遵循 as‑if‑serial 规则)

as‑if‑serial 规则:单线程环境下,重排序后执行结果不变,仅多线程并发场景会出现错乱。

  1. JIT 编译器重排序:JVM 运行期优化字节码,重排机器码执行顺序
  2. CPU 硬件重排序:CPU 流水线乱序执行无依赖的指令,打满硬件性能
补充

javac 源码编译为字节码的阶段,几乎不会发生指令重排序。

三、内存屏障(JMM 底层核心)

内存屏障是 CPU 硬件级特殊指令,是 JMM 实现可见性、有序性的底层支撑,可理解为「指令隔离墙」。

核心规则

  1. 禁止指令跨屏障重排序,保障多线程有序性
  2. 屏障同侧、无依赖的指令,仍可正常重排序优化,兼顾执行性能

四类基础内存屏障

  1. LoadLoad:禁止 读‑读 操作跨屏障重排
  2. LoadStore:禁止 读‑写 操作跨屏障重排
  3. StoreStore:禁止 写‑写 操作跨屏障重排
  4. StoreLoad:全能屏障,开销最大、功能最强,唯一真实生效的硬件屏障

StoreLoad 全能屏障(重中之重)

  1. 强制排空本地 CPU Store‑Buffer 所有积压写操作,广播缓存失效消息
  2. 等待其他所有 CPU 核心返回 ACK 回执,完成数据落地
  3. 全面禁止四种指令重排序,彻底隔离前后所有读写操作

关键误区

StoreLoad 仅等待对方 ACK 回执,不会等待对方 CPU 实际作废缓存,对方缓存失效操作会延后至读数据时执行。

x86 与 ARM 硬件差异

  1. x86 架构:硬件仅允许 Store‑Load 重排序,其余三种重排序被硬件禁止,仅 StoreLoad 有硬件开销,性能极高
  2. ARM 架构:四种重排序全部允许,四类屏障均有硬件开销,volatile 在 ARM 设备性能损耗更大

四、Volatile 关键字底层原理

核心能力总结

✅ 保证可见性、✅ 保证有序性、❌ 不保证原子性

1. 可见性实现原理(读写双向机制)

Volatile 写操作(写端)
  1. 线程修改数据,优先写入本地 CPU Store‑Buffer
  2. 插入 StoreStore 屏障,禁止前置普通写操作重排到 volatile 写之后
  3. 执行 volatile 写,向所有 CPU 核心广播缓存失效消息
  4. 插入 StoreLoad 全能屏障,等待其他核心 ACK 回执,排空写缓冲区,数据落地缓存
Volatile 读操作(读端)
  1. 插入 LoadLoad、LoadStore 双重内存屏障
  2. 优先处理本机 Invalidate‑Queue 所有失效消息,将本地对应缓存行置为无效
  3. 强制从主内存/最新缓存加载数据,保证读取最新值

2. 有序性实现原理

通过四类内存屏障的隔墙约束,禁止 JIT、CPU 指令跨屏障重排序,彻底解决多线程乱序问题,核心用于规避 DCL 单例半初始化 Bug。

3. 为什么 Volatile 不保证原子性

volatile 仅能保障单次读写操作的线程安全。对于 i++ 这类读‑改‑写 复合操作,会拆解为多条 CPU 指令,线程切换可随时打断操作,因此无法保证原子性。

五、Happens‑Before 先行发生原则

核心定义

Happens‑Before 是 JMM 提供给开发者的上层逻辑可见性判断规则,并非 CPU 真实物理执行时序。

规则核心:若操作 A happens‑before 操作 B,那么 A 的所有数据修改,对 B 完全可见;若无该关系,JMM 不保证多线程数据一致性。

只要不破坏 Happens‑Before 规则,JIT、CPU 仍可自由重排序优化。

高频六大核心规则

  1. 程序次序规则:单线程内,书写在前的代码先行发生于后续代码(仅逻辑有序,硬件可重排)
  2. volatile 变量规则:volatile 写操作,先行发生于后续对该变量的所有读操作
  3. 锁规则:锁的解锁操作,先行发生于后续同一把锁的加锁操作
  4. 线程启动规则:Thread.start() 先行发生于新线程内部所有操作
  5. 线程结束规则:线程内所有操作,先行发生于 thread.join() 方法返回
  6. 传递性规则:A happens‑before B、B happens‑before C,则 A happens‑before C(最常用核心规则)

JMM 完整层级链路

硬件层(CPU缓存、MESI协议、双缓冲区、CPU重排序)

JVM层(内存屏障)

语言层(volatile、synchronized)

开发者层(Happens‑Before 可见性规则)

六、Final 关键字 JMM 并发语义

核心作用

final 关键字可禁止指令重排序,防止对象、字段半初始化,保障不可变对象的线程安全。

底层原理

JVM 会在构造方法完全执行结束的位置,插入专属禁止重排序内存屏障。

核心约束:禁止构造器内部 final 字段的赋值操作,重排序到「对象引用向外发布」之后。 简单来说:对象引用暴露给外部线程前,所有 final 字段必须初始化完成。

Final 唯一致命漏洞:构造器泄露 this 引用

final 的线程安全保障,存在硬性前提:必须等待对象构造完全结束,再对外发布对象引用。

若构造方法未执行完毕,提前泄露 this 引用,会彻底打破 final 安全机制:

  1. final 专属内存屏障未生效,重排序约束失效
  2. 外部线程获取到半成品对象
  3. 极端场景下,final 字段会读到默认初始值,出现半初始化问题

漏洞演示代码

public class FinalLeakDemo {
    private final int num;

    public FinalLeakDemo() {
        num = 100;
        // 构造器未执行完毕,泄露this引用,触发线程安全问题
        new Thread(() -> {
            System.out.println(this.num); // 极端情况读到默认值0
        }).start();
    }
}

常见 this 泄露业务场景

  1. 构造方法内启动新线程,传递 this 引用
  2. 构造方法将 this 存入静态集合、静态变量
  3. 构造方法注册监听器、回调函数,向外暴露 this

开发强制规范

构造方法内部严禁泄露 this 引用,必须等待对象构造完成后,再对外发布引用。

七、综合实战:DCL 双重检查锁(JMM 全家桶案例)

线程安全 DCL 单例完整代码

public class Singleton {
    // 必须添加volatile,解决重排序导致的半初始化问题
    private static volatile Singleton instance;

    // 私有构造,禁止外部实例化
    private Singleton(){}

    public static Singleton getInstance(){
        if(instance == null){ // 无锁判断,提升性能
            synchronized (Singleton.class){
                if(instance == null){ // 二次校验,避免重复实例化
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

new 对象底层四步拆解

  1. 分配堆内存空间
  2. 内存默认初始化(所有字段置 0/null)
  3. 执行构造方法,完成字段真实初始化
  4. 将堆内存地址赋值给引用变量 instance

无 volatile 的安全隐患

JIT、CPU 会重排序指令,执行顺序变为:1‑2‑4‑3 后果:引用变量提前赋值,构造方法未执行完毕,其他线程获取到非空半初始化对象,引发业务异常。

Volatile 解决 DCL 问题原理

volatile 修饰引用变量后,写操作会插入 StoreStore + StoreLoad 内存屏障,严格禁止「引用赋值」重排序到「构造初始化」之前,保证对象完全初始化后再对外暴露。

Happens‑Before 推导 DCL 正确性

  1. 对象构造初始化 happens‑before volatile 写(instance 赋值)
  2. volatile 写 happens‑before 后续所有线程的 volatile 读
  3. 根据传递性,对象所有初始化操作,对所有读线程完全可见

最终结论

DCL 单例必须搭配 volatile 关键字,才能实现百分百线程安全。

八、JMM 终极总结

JMM 是 Java 抽象内存规范,核心解决多线程可见性、原子性、有序性三大并发问题。可见性问题源于 CPU 为优化性能设计的 Store‑Buffer 和 Invalidate‑Queue 异步缓冲区,导致 MESI 缓存一致性出现延迟;volatile 通过读写两端的内存屏障,实现多线程数据可见性。

有序性问题由 JIT 编译器优化和 CPU 硬件乱序执行导致,内存屏障作为硬件指令,通过禁止跨屏障重排序兼顾安全与性能,其中 StoreLoad 全能屏障开销最大。volatile 仅保证单次读写的可见性与有序性,无法保证复合操作的原子性。

final 关键字通过构造结束处的内存屏障,防止对象半初始化,保障不可变对象线程安全,唯一漏洞是构造器泄露 this 引用会破坏其安全性。

Happens‑Before 是上层简易可见性判断规则,无需关注硬件细节,底层依靠内存屏障落地,最经典的应用就是 DCL 双重检查锁,必须结合 volatile 才能彻底解决重排序引发的并发安全问题。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐