第53篇:volatile与JMM(2026版)
📌 系列导航 :《Java 100 天进阶之路》完整目录 |
⬅️ 上一篇:第52篇:synchronized深度解析 |
➡️ 下一篇:第54篇:AQS抽象队列同步器(待发布)
🗺️ 本文阅读地图(3 分钟速览)
第52篇吃透了 synchronized,本篇深入 Java 内存模型(JMM) 和 volatile------一个是规范,一个是实现。搞懂这对组合,就能理解 Java 并发 90% 诡异问题的根源:
| 模块 | 核心问题 | 一句话回答 |
|---|---|---|
| JMM是什么 | 和 JVM 内存结构是一回事吗? | 不是。JVM 内存结构回答"数据存哪里",JMM 回答"线程之间怎么看到彼此的修改" |
| 三大特性 | 并发编程要解决什么问题? | 原子性、可见性、有序性------JMM 的终极目标就是为这三点提供规范保障 |
| volatile 保证什么 | volatile 能解决所有并发问题吗? | 保证可见性和有序性,但不保证原子性------volatile 是"轻量级锁" |
| 底层实现 | volatile 凭什么能做到? | 内存屏障 + MESI 缓存一致性协议 |
| 面试最爱问 | 高频考点有哪些? | 见文末 🎤 小节 |
文章目录
-
- 第53篇:volatile与JMM(2026版)
-
- [🗺️ 本文阅读地图(3 分钟速览)](#🗺️ 本文阅读地图(3 分钟速览))
- 一、核心知识点
- 二、生活类比:从"图书馆"到"便利贴墙"
- [三、从 CPU 缓存到 JMM:可见性问题的根源](#三、从 CPU 缓存到 JMM:可见性问题的根源)
-
- [3.1 硬件层面的问题](#3.1 硬件层面的问题)
- [3.2 JMM 的抽象结构](#3.2 JMM 的抽象结构)
- [3.3 普通变量的可见性问题](#3.3 普通变量的可见性问题)
- [四、volatile 的内存语义](#四、volatile 的内存语义)
-
- [4.1 volatile 写:立即刷新到主内存](#4.1 volatile 写:立即刷新到主内存)
- [4.2 volatile 读:强制从主内存读取](#4.2 volatile 读:强制从主内存读取)
- [五、内存屏障:volatile 的底层实现](#五、内存屏障:volatile 的底层实现)
- [六、Happens-Before 原则](#六、Happens-Before 原则)
- [七、volatile vs synchronized](#七、volatile vs synchronized)
- 八、生产级避坑清单
- 九、面试高频考点
- 面试官追问陷阱(加分题)
- 十、练习题
- [📊 你的学习进度](#📊 你的学习进度)
- [👉 下一篇文章预告](#👉 下一篇文章预告)
一、核心知识点
JMM(Java Memory Model,Java 内存模型) 是 Java 语言规范定义的一套内存访问规则,用于屏蔽不同硬件和操作系统的内存访问差异,保证 Java 程序在所有平台上并发行为一致。重要区分:JMM ≠ JVM 内存结构------JVM 内存结构(堆、栈、方法区等)解决"数据存在哪里、怎么存"的问题;JMM 解决"多线程下数据什么时候可见、执行顺序如何保证"的问题。
volatile 的核心特性:
- ✅ 保证可见性:volatile 变量的修改立即刷新到主内存,其他线程读取时强制从主内存获取最新值
- ✅ 禁止指令重排序:volatile 读写前后插入内存屏障,防止编译器/CPU 重排
- ❌ 不保证原子性 :
i++等复合操作仍线程不安全
JMM 的三大核心问题:
| 特性 | 问题根源 | 核心定义 |
|---|---|---|
| 原子性 | 线程切换导致操作中断 | 一个操作要么全部执行,要么完全不执行,不会被线程调度打断 |
| 可见性 | CPU 多级缓存、缓存一致性协议延迟 | 一个线程修改共享变量,其他线程能立即看到最新值 |
| 有序性 | 编译器、CPU 的指令重排优化 | 程序执行顺序和代码编写顺序保持一致 |
二、生活类比:从"图书馆"到"便利贴墙"
JMM 就像一个图书馆的图书借阅系统 :

- 主内存:图书馆中央的藏书架(所有书都在这里)
- 工作内存(本地内存):每个人手里的借阅记录本(自己抄录的书目)
- 线程:不同的读者
普通变量(非 volatile)的行为:读者 A 从书架上抄了一份书单到自己本子上,然后在上面画了标记(修改),但没有把修改后的书单放回书架(没刷新到主内存)。读者 B 来借书时,拿着自己旧本子上的信息去找书,根本不知道 A 改了什么。
volatile 的规则:
- 写规则 :任何人在自己的本子上画了标记(修改 volatile 变量),必须立即把修改后的内容贴到图书馆中央的便利贴墙(主内存)上,所有人都能看到
- 读规则 :任何人要看便利贴墙上的内容,必须先把本子上对应的旧记录撕掉(本地缓存失效),然后去墙上看最新版本
这就是 volatile 保证可见性的全部秘密------强制"写即刷、读即取",不给缓存留活路。
指令重排就像厨房做饭的"优化" :顺序是"洗锅→接水→等水烧开→洗米→切菜",实际执行时可以"等水烧开"的间隙"洗米、切菜",最终结果不变。单线程没问题,多线程下这种"优化"会导致依赖执行顺序的逻辑出错------volatile 的内存屏障就像给厨房加了"必须先做A再做B"的硬性规定。
三、从 CPU 缓存到 JMM:可见性问题的根源
3.1 硬件层面的问题
现代 CPU 运算速度比主内存快 3-4 个数量级,为了弥补这个速度鸿沟,CPU 引入了多级高速缓存(L1/L2/L3)。多核 CPU 下,每个核心都有自己的私有缓存,同一个变量可能在多个核心的缓存中都有副本。当 CPU1 修改了变量的缓存副本,CPU2 的缓存副本还是旧值------出现缓存数据不一致问题。
3.2 JMM 的抽象结构

JMM 规定:
- 所有共享变量存储在主内存中(物理内存的抽象)
- 每个线程有自己私有的工作内存(CPU 寄存器、高速缓存的抽象)
- 线程对变量的所有读写,都必须在工作内存中进行,不能直接操作主内存
- 线程间变量传递必须通过主内存
关键纠正:JMM 的工作内存 ≠ JVM 栈内存,主内存 ≠ JVM 堆内存------JMM 是抽象的内存模型,工作内存是对 CPU 寄存器、高速缓存的抽象,主内存是对物理内存的抽象。
3.3 普通变量的可见性问题
普通变量不会强制同步到主内存,JMM 只在特定时机(如线程切换、方法退出等)将工作内存的修改刷新到主内存。这导致:
- 线程 A 修改了变量,但修改还在工作内存中
- 线程 B 读到的仍是主内存中的旧值
- 出现可见性问题
四、volatile 的内存语义
volatile 的内存语义定义了它在主内存和线程本地内存之间的操作规则:
4.1 volatile 写:立即刷新到主内存
线程对 volatile 变量执行写操作时,JMM 强制要求:必须立即将本地内存中修改后的值刷新到主内存。普通变量的修改可能"藏"在本地内存中,而 volatile 写相当于一个"刷新信号"------只要执行了这个写操作,就必须把本地的修改同步到主内存。
4.2 volatile 读:强制从主内存读取
线程对 volatile 变量执行读操作时,JMM 强制要求:立即将本地内存中该变量的副本置为无效,然后从主内存读取最新值。volatile 写的"刷新"和 volatile 读的"读取"共同构成了可见性的完整保障。
五、内存屏障:volatile 的底层实现
内存屏障(Memory Barrier)是一组 CPU 指令,用来控制内存操作的顺序和可见性。JMM 在 volatile 读写前后插入内存屏障,禁止特定类型的重排序。
JMM 保守策略下的内存屏障插入规则 :

四种内存屏障的作用:
| 屏障类型 | 作用 |
|---|---|
| LoadLoad | 禁止 Load1 和 Load2 重排------Load1(volatile读)先执行,Load2(普通读)后执行 |
| StoreStore | 禁止 Store1 和 Store2 重排------Store1(普通写)先执行,Store2(volatile写)后执行 |
| LoadStore | 禁止 Load1 和 Store2 重排------Load1(volatile读)先执行,Store2(普通写)后执行 |
| StoreLoad | 禁止 Store1 和 Load2 重排------Store1(volatile写)先执行,Load2(普通读)后执行,开销最大 |
六、Happens-Before 原则
Happens-Before 是 JMM 提供给程序员的上层规范------如果操作 A happens-before 操作 B,那么 A 所做的所有更改在 B 中一定可见。
与 volatile 直接相关的规则:
volatile 变量规则:对一个 volatile 变量的写操作,happens-before 于后续对该变量的所有读操作。这意味着:写入 volatile 变量之前的所有操作(包括对其他普通变量的修改),在读取该 volatile 变量的线程中全部可见。这就是 volatile 能"传递"可见性的根本原因。
其他 happens-before 规则:
- 程序顺序规则:同一线程内,前面的操作 happens-before 后面的操作
- 锁规则:解锁操作 happens-before 后续的加锁操作
- 传递性:A happens-before B,B happens-before C,则 A happens-before C
DCL 单例必须加 volatile 的根源 :instance = new Singleton() 在字节码层面包含三个步骤(分配内存、初始化对象、将引用指向内存地址),编译器可能重排后两步(先指向内存地址再初始化)。如果不加 volatile,其他线程可能读取到半初始化的对象,触发 NPE。volatile 通过内存屏障禁止了这种重排序。
七、volatile vs synchronized
| 对比维度 | volatile | synchronized |
|---|---|---|
| 原子性 | ❌ 不保证 | ✅ 保证 |
| 可见性 | ✅ 保证 | ✅ 保证 |
| 有序性 | ✅ 禁止重排 | ✅ 保证 |
| 锁机制 | 无锁,内存语义约束 | 排他锁,OS 级互斥 |
| 性能开销 | 低(比普通变量慢约 20%) | 高(涉及用户态/内核态切换) |
| 适用场景 | 状态标记、DCL、轻量级状态更新 | 复合操作、需要原子性的场景 |
| 可重入性 | 不涉及 | 支持 |
八、生产级避坑清单
text
✅ volatile 使用避坑指南
1. volatile 不保证原子性 → i++、count++ 等复合操作用 AtomicInteger 或 synchronized
2. volatile 不能替代 synchronized → 需要原子性时用锁或 JUC 工具类
3. 锁对象不能用 volatile 修饰 → volatile 只修饰共享变量,不修饰锁对象
4. 注意变量依赖关系 → volatile 保证的是变量自身的可见性,不保证依赖该变量的其他变量
5. DCL 单例必须加 volatile → 防止指令重排导致半初始化对象暴露
6. 虚拟线程中 volatile 无特殊限制 → 但复合操作仍需原子性保障
九、面试高频考点
Q1:volatile 能保证原子性吗?为什么?
不能 。volatile 只保证变量的读写操作(单次操作)是原子的,但像
count++这种"读-改-写"复合操作,在字节码层面包含getfield、iadd、putfield三个步骤,volatile 无法保证这三步不被其他线程打断。
Q2:volatile 如何保证可见性?
volatile 写操作会立即将本地内存的修改刷新到主内存;volatile 读操作会立即将本地内存的副本置为无效,强制从主内存读取最新值。两者配合,通过 MESI 缓存一致性协议实现跨线程可见性。
Q3:volatile 如何禁止指令重排序?
JMM 在 volatile 读写前后插入内存屏障。volatile 写前插入 StoreStore 屏障,写后插入 StoreLoad 屏障;volatile 读后插入 LoadLoad 和 LoadStore 屏障,禁止特定类型的重排序。
Q4:DCL 单例为什么必须加 volatile?
new Singleton()包含三个步骤:① 分配内存、② 初始化对象、③ 将引用指向内存地址。编译器和 CPU 可能将 ② 和 ③ 重排。如果线程 A 执行到 ③ 但未执行 ②,线程 B 读取到引用后发现不为 null,直接返回半初始化对象,触发 NPE。volatile 禁止了这种重排序。
Q5:JMM 和 JVM 内存结构的区别?
JVM 内存结构回答"数据存在哪里"------堆、栈、方法区等。JMM 回答"线程之间怎么看到彼此的修改"------可见性、有序性、原子性的规范。两者完全不同,切勿混淆。
Q6:什么场景适合用 volatile?
① 状态标记 :
volatile boolean stop = false;② DCL 单例 :保证对象安全发布;③ 无需原子性的状态更新:一个线程写、多个线程读的轻量级状态。不适合复合操作。
面试官追问陷阱(加分题)
追问1:"volatile 比普通变量慢多少?为什么?"
👉 约慢 20%。原因是 volatile 读写每次都要绕过缓存直接操作主内存,并插入内存屏障阻止重排序优化,增加了内存访问延迟。
追问2 :"volatile 和 AtomicInteger 有什么区别?什么时候用哪个?"
👉
volatile只保证可见性,不保证原子性;AtomicInteger通过 CAS 保证原子性。单纯状态标记用 volatile,计数等复合操作用 AtomicInteger。
追问3:"volatile 能保证 64 位(long/double)的原子性吗?"
👉 可以。JVM 规范保证对 volatile 修饰的 long/double 的读写是原子的。普通 long/double 在 32 位 JVM 上可能被拆分为两次 32 位操作,但 volatile 强制原子读写。
十、练习题
-
分析题 :下面代码中,线程 B 一定能看到线程 A 对
x和flag的修改吗?为什么?javaint x = 0; volatile boolean flag = false; // 线程 A x = 42; flag = true; // 线程 B if (flag) { System.out.println(x); }💡 思路:flag 是 volatile,A 写 flag 之前的所有操作(包括 x=42)对 B 可见,B 看到 flag=true 时一定能看到 x=42。
-
代码题:用 volatile 实现一个简单的"停止工作线程"的状态标志,并验证可见性。
-
场景设计 :某系统中使用
volatile int count做计数器,多个线程调用count++,发现结果总是不正确。为什么?如何修复?💡 思路:
count++非原子操作。修复方案:改用AtomicInteger或synchronized。
📊 你的学习进度
- 当前:第53篇 / 共108篇 · 进阶篇:并发编程与JUC详解(第51~60篇)
- ✅ 已完成:基础篇44篇 + 第45~53篇
- 📖 正在学:第53篇
- ⏳ 待学习:第54~108篇
👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇
👉 下一篇文章预告
🚀 下一篇:《第54篇:AQS抽象队列同步器》
内容简介:AQS 核心原理(state、CLH 队列、独占/共享模式)、ReentrantLock 实现、Semaphore 和 CountDownLatch 底层分析。
👉 多线程专题持续深入,拿下 JUC 底层基石!
📌 《Java 100 天进阶之路 | 从入门到上岗就业》 每天一篇,建议收藏 + 关注 ,一起100天拿offer!
👉 点击关注我,更新后第一时间收到推送!