软件技术 · 知识点深化 · 并发编程
Java 内存模型与 volatile:可见性、有序性、禁止重排
一个变量加了 volatile,到底解决了什么?很多人背得出"可见性、有序性",却说不清为什么不能保证原子性。这一页从 JMM 主内存/工作内存讲起,把 happens-before、指令重排、内存屏障一次讲透,彻底搞懂 volatile 的边界。
① 怎么学(4 步走,约 65 分钟)
先建立直觉再抠细节,按这四步走最稳:
1看图建立直觉(10 分钟)
读②③:先在脑子里画出本课核心结构图。
2记完整体系(15 分钟)
读④:把对比表和公式看懂,不要急着背。
3跟例题走一遍(20 分钟)
精读⑤:看三个例题怎么用知识点解题。
4刷题纠错(剩余时间)
做⑦⑩,错题回⑥诊断。
本课小目标学完你要能:① 解释 JMM 的主内存与工作内存;② 说出 volatile 的两大作用;③ 说明 volatile 为什么不保证 i++ 安全;④ 列三条 happens-before 规则。
② 一图看懂:JMM 与 volatile 全景
读法:中心是本课主题,四条分支展开核心维度,下方是典型应用与高频易错点。
③ 本质直觉:JMM 就是"各写各的笔记本,定期对黑板"
把主内存想成教室前面的黑板,每个线程有自己的笔记本。线程干活时先把黑板上的数据抄到笔记本上,改完之后不立即写回黑板。问题来了:线程 A 改了笔记本上的数字,线程 B 看的还是黑板上的旧值——这就是可见性问题。
volatile 的作用:告诉 JVM,这个变量别用笔记本,直接读写黑板。一个线程改了立即写回黑板,其他线程读的时候直接从黑板读,保证大家看到的是最新值。
但 volatile 不保证原子性:i++ 是三步操作:读 i→加 1→写回。即使每次读都从黑板读,线程 A 读到 5 后,线程 B 也读到 5,两个都加 1 写回 6,丢了一次。volatile 只保证你读到的是新值,不保证"读-改-写"是一气呵成。
有序性:CPU 和编译器为了提速会重排指令。volatile 写操作前后插入内存屏障,禁止重排,保证 DCL 单例中对象初始化完成后引用才赋值。
happens-before 八条规则程序次序规则、监视器锁规则、volatile 变量规则、线程启动规则、线程终止规则、线程中断规则、对象终结规则、传递性。前四条面试最高频。
④ 完整知识体系
三大特性与 volatile 边界
| 特性 | 含义 | volatile 是否保证 |
| 可见性 | 一个线程改了,其他线程立刻看到 | 保证 |
| 有序性 | 禁止指令重排 | 保证(插入内存屏障) |
| 原子性 | 读-改-写不可分割 | 不保证 |
| synchronized | 三大特性全保证 | 保证(但开销大) |
| Lock | 三大特性全保证 | 保证 |
| 普通变量 | 都不保证 | — |
happens-before volatile对 volatile 写 happens-before 后续对它的读
DCL 单例instance = new Singleton() 必须 volatile,防止重排
i++ 字节码读 getstatic → 加 1 iadd → 写 putstatic,三步非原子
指令重排示例
一个对象创建:memory = allocate(); init(memory); instance = memory;。JIT 可能重排成 memory=allocate(); instance=memory; init(memory);。此时 instance 已非空但对象还没初始化,另一个线程拿到半成品。
内存屏障类型
LoadLoad:先读再读,禁止后面的读重排到前面。
StoreStore:先写再写,保证前面写对其他处理器可见后才写后面。
volatile 写前插 StoreStore,写后插 LoadLoad;volatile 读前后也有屏障。
volatile 适用场景状态标志位(一写多读)、DCL 单例。不适用:计数器、复合操作、需要互斥的场景——那些用 synchronized 或原子类。
⑤ 应用场景与例题
例1 volatile 能保证 i++ 线程安全吗?
两个线程各 i++ 1000 次,最终结果是 2000 吗?
不能。i++ 是读-改-写三步,volatile 只保证每次读到最新值,但两个线程可能同时读到同一个旧值,各自加 1 写回,结果小于 2000。需要 AtomicInteger 或 synchronized。
例2 为什么 DCL 单例要 volatile?
不加会怎样?
new 对象三步可能被重排为分配内存→赋值引用→初始化。另一个线程判断引用非空直接返回,拿到未初始化对象。volatile 禁止重排。
例3 boolean stop 标志位加 volatile 为什么有用?
不加会怎样?
一写多读的停止标志:主线程改 stop=true,工作线程循环检查 stop。不加 volatile,工作线程可能一直在自己的工作内存里读 false,永远停不下来。
做题心法volatile 管"看得见"和"顺序对",不管"一步到位"。
⑥ 高频错误诊断(4 条)
错误1:以为 volatile 能保证线程安全i++、复合操作仍然不安全,需要加锁或原子类。
错误2:以为 volatile 能替代 synchronizedvolatile 不保证原子性和互斥,临界区仍需锁。
错误3:所有变量都加 volatile无谓开销,volatile 会阻止 JIT 优化,只在一写多读标志位用。
错误4:DCL 单例忘记 volatile偶发 NPE 或半初始化对象,极难排查。
⑦ 考点真题演练(5 题)
考点分布
| 考法 | 出题形式 | 应对 |
| 三大特性 | 问 volatile 保哪个 | 可见性+有序性,不保证原子性 |
| 重排 | 问 DCL 为什么要 volatile | 防止对象半初始化 |
| happens-before | 问规则 | volatile 写先于读 |
| 适用场景 | 什么变量适合 volatile | 一写多读标志位 |
真题basic1. volatile 不保证以下哪个?
真题mid2. DCL 单例中 instance 字段为什么要 volatile?
真题mid3. 关于 i++ 加 volatile,正确的是?
真题hard4. happens-before 规则中,volatile 写和读的关系是?
真题hard5. 以下哪个场景最适合用 volatile?
⑧ 必背知识点卡
JMM:主内存+工作内存,线程操作副本
volatile:可见性+有序性,不保证原子性
i++:读-改-写三步,volatile 救不了 用 AtomicInteger
重排:new 对象三步可能重排,volatile 禁掉 DCL 必备
内存屏障:StoreStore/LoadLoad 禁止重排
happens-before:volatile 写先于读,传递性
适用:一写多读标志位,别滥用
⑨ 动手输出:向面试官解释 JMM 三大特性
场景:面试官问"你说说可见性、原子性、有序性"。
① 可见性:一个线程修改共享变量,其他线程能立刻看到。volatile 和 synchronized 都能保证。
② 原子性:操作不可分割,中间不会被插队。i++ 不是原子的,需要 synchronized 或 AtomicInteger。
③ 有序性:代码按写的顺序执行,不会被 CPU/编译器重排。volatile 通过内存屏障禁止重排。
④ 举例:DCL 单例就是可见性+有序性的组合场景。
⑤ 边界:volatile 只管前两个半(可见性全管,有序性部分管),原子性不管。
口述思路"volatile 让你看得到、顺序对,但不帮你一气呵成。"
⑩ 分层练习(基础 + 中档 + 拔高)
▍基础 6 题
基础1JMM 中线程私有的叫什么?
工作内存(栈帧/本地变量)。
基础2volatile 保证哪两个特性?
可见性和有序性。
基础4i++ 是原子操作吗?
不是,读-改-写三步。
基础5DCL 单例 instance 为什么要 volatile?
防止 new 对象指令重排。
基础6happens-before 是什么?
偏序关系规则,前一个操作的结果对后一个可见。
▍中档 5 题
中档1为什么工作内存会导致可见性问题?
线程从主内存读副本到本地,修改后不立即写回,其他线程看不到。
中档2volatile 写后其他线程怎么看到?
volatile 写会立即刷主内存,并使其他线程工作内存中的缓存行失效。
中档3StoreStore 屏障作用?
保证屏障前的写操作先于屏障后的写操作对其他处理器可见。
中档4synchronized 怎么保证可见性?
进入时清空工作内存从主存读,退出时刷回主存。
中档5AtomicInteger 底层靠什么?
CAS + volatile,保证读-改-写原子。
▍拔高 5 题
拔高1为什么说 volatile 不保证原子性?
读和写各自是可见的,但中间的"改"没有锁,可能被其他线程插队。
拔高2指令重排在什么层面发生?
编译器、JIT、CPU 内存模型三层都可能重排。
拔高3happens-before 传递性是什么?
A happens-before B,B happens-before C,则 A happens-before C。
拔高4volatile 变量的读在 JVM 里插什么屏障?
LoadLoad 屏障,禁止后续读重排到前面。
拔高5什么是伪共享(False Sharing)?
多个 volatile 变量在同一个缓存行,一个变其他缓存行失效,性能下降。用 long padding 解决。
⑪ 记忆口诀 + 7 天复习计划
三句口诀① 主存黑板工作本,volatile 直接走黑板。② 可见有序它都管,原子性它不管。③ i++ 要用原子类,DCL 必须加 volatile。
| 天 | 任务 | 自检 |
| 第 1 天 | 读②③,画 JMM 读写流程图 | 主存工作内存说清 |
| 第 2 天 | 背三大特性表 + 基础 6 题 | volatile 边界说清 |
| 第 3 天 | 做中档 5 题,手写 DCL 单例 | 代码正确 |
| 第 4 天 | 做拔高 5 题,解释内存屏障 | 能讲 StoreStore |
| 第 5 天 | 做⑦真题 5 题 | 限时每题 2 分钟 |
| 第 6-7 天 | 合书口述 happens-before 规则 | 不看资料全对 |
← 返回软件技术总览