一文打尽 JMM:Java内存模型全套笔记
目录
一、什么是JMM
JMM 全称 Java Memory Model(Java 内存模型),是 JVM 官方定义的一套抽象内存规范、并发内存语义标准,并非真实的内存结构。
JMM 的核心作用:屏蔽不同硬件(x86、ARM)、不同操作系统的内存差异,统一规范 Java 多线程的内存行为,专门解决多线程并发编程的三大核心问题:可见性、原子性、有序性。
核心前提: 单线程环境下,JMM 的所有问题都不会暴露,程序执行完全符合代码逻辑。所有 Java 并发 Bug,本质都是多线程共享变量交互引发的,全部由 JMM 三大特性缺失导致。
二、并发三大核心特性(并发问题根源)
1. 可见性
概念
多个线程共享同一个变量,一个线程修改了共享变量的值,其他线程无法立刻感知到最新修改值,始终读取本地旧缓存数据,导致数据不一致。
底层硬件根源
- CPU 多级缓存架构:每个 CPU 核心拥有私有 L1、L2 缓存,多核共享 L3 缓存与主内存。线程优先操作 CPU 私有缓存,多核缓存相互隔离,数据无法实时同步。
- 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 规则:单线程环境下,重排序后执行结果不变,仅多线程并发场景会出现错乱。
- JIT 编译器重排序:JVM 运行期优化字节码,重排机器码执行顺序
- CPU 硬件重排序:CPU 流水线乱序执行无依赖的指令,打满硬件性能
补充
javac 源码编译为字节码的阶段,几乎不会发生指令重排序。
三、内存屏障(JMM 底层核心)
内存屏障是 CPU 硬件级特殊指令,是 JMM 实现可见性、有序性的底层支撑,可理解为「指令隔离墙」。
核心规则
- 禁止指令跨屏障重排序,保障多线程有序性
- 屏障同侧、无依赖的指令,仍可正常重排序优化,兼顾执行性能
四类基础内存屏障
- LoadLoad:禁止 读‑读 操作跨屏障重排
- LoadStore:禁止 读‑写 操作跨屏障重排
- StoreStore:禁止 写‑写 操作跨屏障重排
- StoreLoad:全能屏障,开销最大、功能最强,唯一真实生效的硬件屏障
StoreLoad 全能屏障(重中之重)
- 强制排空本地 CPU Store‑Buffer 所有积压写操作,广播缓存失效消息
- 等待其他所有 CPU 核心返回 ACK 回执,完成数据落地
- 全面禁止四种指令重排序,彻底隔离前后所有读写操作
关键误区
StoreLoad 仅等待对方 ACK 回执,不会等待对方 CPU 实际作废缓存,对方缓存失效操作会延后至读数据时执行。
x86 与 ARM 硬件差异
- x86 架构:硬件仅允许 Store‑Load 重排序,其余三种重排序被硬件禁止,仅 StoreLoad 有硬件开销,性能极高
- ARM 架构:四种重排序全部允许,四类屏障均有硬件开销,volatile 在 ARM 设备性能损耗更大
四、Volatile 关键字底层原理
核心能力总结
✅ 保证可见性、✅ 保证有序性、❌ 不保证原子性
1. 可见性实现原理(读写双向机制)
Volatile 写操作(写端)
- 线程修改数据,优先写入本地 CPU Store‑Buffer
- 插入 StoreStore 屏障,禁止前置普通写操作重排到 volatile 写之后
- 执行 volatile 写,向所有 CPU 核心广播缓存失效消息
- 插入 StoreLoad 全能屏障,等待其他核心 ACK 回执,排空写缓冲区,数据落地缓存
Volatile 读操作(读端)
- 插入 LoadLoad、LoadStore 双重内存屏障
- 优先处理本机 Invalidate‑Queue 所有失效消息,将本地对应缓存行置为无效
- 强制从主内存/最新缓存加载数据,保证读取最新值
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 仍可自由重排序优化。
高频六大核心规则
- 程序次序规则:单线程内,书写在前的代码先行发生于后续代码(仅逻辑有序,硬件可重排)
- volatile 变量规则:volatile 写操作,先行发生于后续对该变量的所有读操作
- 锁规则:锁的解锁操作,先行发生于后续同一把锁的加锁操作
- 线程启动规则:
Thread.start()先行发生于新线程内部所有操作 - 线程结束规则:线程内所有操作,先行发生于
thread.join()方法返回 - 传递性规则: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 安全机制:
- final 专属内存屏障未生效,重排序约束失效
- 外部线程获取到半成品对象
- 极端场景下,final 字段会读到默认初始值,出现半初始化问题
漏洞演示代码
public class FinalLeakDemo {
private final int num;
public FinalLeakDemo() {
num = 100;
// 构造器未执行完毕,泄露this引用,触发线程安全问题
new Thread(() -> {
System.out.println(this.num); // 极端情况读到默认值0
}).start();
}
}
常见 this 泄露业务场景
- 构造方法内启动新线程,传递 this 引用
- 构造方法将 this 存入静态集合、静态变量
- 构造方法注册监听器、回调函数,向外暴露 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 对象底层四步拆解
- 分配堆内存空间
- 内存默认初始化(所有字段置 0/null)
- 执行构造方法,完成字段真实初始化
- 将堆内存地址赋值给引用变量 instance
无 volatile 的安全隐患
JIT、CPU 会重排序指令,执行顺序变为:1‑2‑4‑3 后果:引用变量提前赋值,构造方法未执行完毕,其他线程获取到非空半初始化对象,引发业务异常。
Volatile 解决 DCL 问题原理
volatile 修饰引用变量后,写操作会插入 StoreStore + StoreLoad 内存屏障,严格禁止「引用赋值」重排序到「构造初始化」之前,保证对象完全初始化后再对外暴露。
Happens‑Before 推导 DCL 正确性
- 对象构造初始化 happens‑before volatile 写(instance 赋值)
- volatile 写 happens‑before 后续所有线程的 volatile 读
- 根据传递性,对象所有初始化操作,对所有读线程完全可见
最终结论
DCL 单例必须搭配 volatile 关键字,才能实现百分百线程安全。
八、JMM 终极总结
JMM 是 Java 抽象内存规范,核心解决多线程可见性、原子性、有序性三大并发问题。可见性问题源于 CPU 为优化性能设计的 Store‑Buffer 和 Invalidate‑Queue 异步缓冲区,导致 MESI 缓存一致性出现延迟;volatile 通过读写两端的内存屏障,实现多线程数据可见性。
有序性问题由 JIT 编译器优化和 CPU 硬件乱序执行导致,内存屏障作为硬件指令,通过禁止跨屏障重排序兼顾安全与性能,其中 StoreLoad 全能屏障开销最大。volatile 仅保证单次读写的可见性与有序性,无法保证复合操作的原子性。
final 关键字通过构造结束处的内存屏障,防止对象半初始化,保障不可变对象线程安全,唯一漏洞是构造器泄露 this 引用会破坏其安全性。
Happens‑Before 是上层简易可见性判断规则,无需关注硬件细节,底层依靠内存屏障落地,最经典的应用就是 DCL 双重检查锁,必须结合 volatile 才能彻底解决重排序引发的并发安全问题。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)