《Java 100 天进阶之路》第53篇:volatile与JMM(2026版)

第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 的规则

  1. 写规则 :任何人在自己的本子上画了标记(修改 volatile 变量),必须立即把修改后的内容贴到图书馆中央的便利贴墙(主内存)上,所有人都能看到
  2. 读规则 :任何人要看便利贴墙上的内容,必须先把本子上对应的旧记录撕掉(本地缓存失效),然后去墙上看最新版本

这就是 volatile 保证可见性的全部秘密------强制"写即刷、读即取",不给缓存留活路。

指令重排就像厨房做饭的"优化" :顺序是"洗锅→接水→等水烧开→洗米→切菜",实际执行时可以"等水烧开"的间隙"洗米、切菜",最终结果不变。单线程没问题,多线程下这种"优化"会导致依赖执行顺序的逻辑出错------volatile 的内存屏障就像给厨房加了"必须先做A再做B"的硬性规定。


三、从 CPU 缓存到 JMM:可见性问题的根源

3.1 硬件层面的问题

现代 CPU 运算速度比主内存快 3-4 个数量级,为了弥补这个速度鸿沟,CPU 引入了多级高速缓存(L1/L2/L3)。多核 CPU 下,每个核心都有自己的私有缓存,同一个变量可能在多个核心的缓存中都有副本。当 CPU1 修改了变量的缓存副本,CPU2 的缓存副本还是旧值------出现缓存数据不一致问题。

3.2 JMM 的抽象结构

JMM 规定:

  1. 所有共享变量存储在主内存中(物理内存的抽象)
  2. 每个线程有自己私有的工作内存(CPU 寄存器、高速缓存的抽象)
  3. 线程对变量的所有读写,都必须在工作内存中进行,不能直接操作主内存
  4. 线程间变量传递必须通过主内存

关键纠正: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++ 这种"读-改-写"复合操作,在字节码层面包含 getfieldiaddputfield 三个步骤,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 :"volatileAtomicInteger 有什么区别?什么时候用哪个?"

👉 volatile 只保证可见性,不保证原子性;AtomicInteger 通过 CAS 保证原子性。单纯状态标记用 volatile,计数等复合操作用 AtomicInteger。

追问3:"volatile 能保证 64 位(long/double)的原子性吗?"

👉 可以。JVM 规范保证对 volatile 修饰的 long/double 的读写是原子的。普通 long/double 在 32 位 JVM 上可能被拆分为两次 32 位操作,但 volatile 强制原子读写。


十、练习题

  1. 分析题 :下面代码中,线程 B 一定能看到线程 A 对 xflag 的修改吗?为什么?

    java 复制代码
    int 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。

  2. 代码题:用 volatile 实现一个简单的"停止工作线程"的状态标志,并验证可见性。

  3. 场景设计 :某系统中使用 volatile int count 做计数器,多个线程调用 count++,发现结果总是不正确。为什么?如何修复?

    💡 思路:count++ 非原子操作。修复方案:改用 AtomicIntegersynchronized


📊 你的学习进度

  • 当前:第53篇 / 共108篇 · 进阶篇:并发编程与JUC详解(第51~60篇)
  • ✅ 已完成:基础篇44篇 + 第45~53篇
  • 📖 正在学:第53篇
  • ⏳ 待学习:第54~108篇

👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇

👉 下一篇文章预告

🚀 下一篇:《第54篇:AQS抽象队列同步器》

内容简介:AQS 核心原理(state、CLH 队列、独占/共享模式)、ReentrantLock 实现、Semaphore 和 CountDownLatch 底层分析。

👉 多线程专题持续深入,拿下 JUC 底层基石!

📌 《Java 100 天进阶之路 | 从入门到上岗就业》 每天一篇,建议收藏 + 关注 ,一起100天拿offer!

👉 点击关注我,更新后第一时间收到推送!

相关推荐
米饭不加菜1 小时前
使用万用表判断三极管(BJT)好坏
java·开发语言
Maynor9962 小时前
AI Coding 零基础实战教程|第五部分:完整项目案例实操
java·前端·人工智能·claude code·ai coding
生戎马2 小时前
Tomcat Container容器之Engine:StandardEngine _
java·tomcat
zhangjw342 小时前
第29篇:Java伪共享与对象分配:并发性能优化的关键
java·开发语言·性能优化
宠友信息2 小时前
内容社区源码多端数据协议与 Spring Boot 状态流转实现思路
java·spring boot·websocket·uni-app
一路向北North3 小时前
Spring Security OAuth2.0(19):JWT令牌
java·后端·spring
玛卡巴卡ldf3 小时前
【LeetCode 手撕算法】(细节知识点总结)
java·数据结构·算法·leetcode·力扣
一叶飘零_sweeeet4 小时前
IDEA 插件 Trae AI 最新全攻略(基于 TRAE AI: Coding Assistant 1.7.0.0)
java·intellij-idea·trae
KobeSacre4 小时前
CyclicBarrier 源码
java·jvm·算法