← ReJava · 学习目录

REJAVA / LEARNING NOTES

volatile(可见性关键字)与 JMM(Java 内存模型)

从共享状态与对象初始化出发,理解可见性、重排序与复合操作的限制。

Java 17 / 21更新于 2026.10.02约 12 分钟阅读

根据课程材料整理

本次更新说明
  • 整理为公开学习笔记,保留技术内容与练习。
  • 清理个人分析,完善阅读导航。

为什么先学这个

本课成果:能用 volatile(可见性关键字)的写—读规则解释共享状态与安全发布,识别自增等复合操作的缺口,并完成一次60秒独立表达。先读懂下面的例子,再检验这些规则能解决什么问题。

需要能读懂字段、方法和两个线程交错的简单代码。下文先定义术语;未学过的同步实现不作本课收束门槛。

一、问题的根源:为什么多线程会出问题

这一段是理解 volatile(可见性关键字) 的前置知识(来自 JavaGuide《Java 内存模型》,已存档 reference/javaguide-jmm.md):

根源发生了什么导致
CPU(处理器) 多级缓存编译器和处理器会优化读写;跨线程访问需遵守同步规则可见性 问题:一个线程改了,另一个看不到
指令重排序编译器/处理器为性能调整指令顺序有序性 问题:多线程下顺序不可预期
复合操作i++ 实际是「读—改—写」三步原子性 问题:并发下丢失更新

重排序需要遵守 Java 内存模型。单线程表现不变,不能单独证明多线程协作正确;并发程序仍需建立规定的同步关系。

先把三个词定义清楚

上面的表格里已经出现了「原子性 / 可见性 / 有序性」,先统一含义。下表参考 JavaGuide(存档 reference/javaguide-jmm.md),其中锁的保护范围按 JDK 对象监视器文档限定(存档 官方摘录 C、D 节):

术语定义Java 里谁来保证
原子性操作作为不可分割的整体。用锁保护复合操作时,相关访问必须遵守同一把锁的协议;同步块不会因异常而自动回滚已执行的修改synchronized(内置同步关键字)、Lock(显式锁接口);原子类保证其指定操作
可见性在相应同步规则成立时,先前写入对后续读取可见;不是任意时刻、任意访问都自动看到最新修改synchronized(内置同步关键字)、volatile(可见性关键字)、Lock(显式锁接口)
有序性并发观察结果应遵守内存模型要求;相关重排序受到约束,不等于禁止一切优化volatile(可见性关键字) 约束相关重排序

happens-before(先行发生关系) 是什么? (本课后面会反复用到)
它不是「时间上先发生」。JavaGuide 的原话是: 它想表达的是前一个操作的结果对于后一个操作是可见的,无论这两个操作是否在同一个线程里。
所以「A happens-before(先行发生关系) B」= A 的结果对 B 可见,而不是「A 的时钟先走到」。

二、JMM(Java 内存模型) 是什么(别和 JVM(Java 虚拟机) 内存区域搞混)

JavaGuide 对这个高频误区的回答很干脆: Java 内存区域和内存模型是完全不一样的两个东西。

  • JVM(Java 虚拟机) 内存区域 :和运行时数据分区有关(堆、栈、方法区……),讲的是「数据放哪」。
  • JMM(Java 内存模型) :和并发编程有关,是一组 规范 ,抽象了线程与主内存的关系,规定从源码到 CPU(处理器) 指令的转化要遵守哪些并发原则。目的是 屏蔽系统差异、简化多线程编程 。

JMM(Java 内存模型) 的两个抽象概念:

  • 主内存 :抽象线程间可共享的变量(实例字段、静态字段、数组元素)。 它是规范层面的抽象,不等同于某一块物理内存。
  • 本地内存 :每个线程私有的副本,涵盖缓存、写缓冲区、寄存器以及编译器优化 —— 它不是一块与某个硬件区域一一对应的独立内存 。

这是帮助理解共享访问的抽象,不是每次访问物理内存的执行步骤。判断某次读取是否有保证,应建立具体的 happens-before(先行发生关系)。

三、核心:volatile(可见性关键字) 的两个语义

本节的措辞依据 JavaGuide 并发常见面试题(中)· volatile(可见性关键字) 关键字(存档:reference/javaguide-concurrent-questions-02.md)。

语义 1 · 可见性

对 volatile(可见性关键字) 变量的 写 happens-before(先行发生关系) 于后续对同一变量的 读 。读线程因此能看到该写入, 以及写入前按 happens-before(先行发生关系) 传递过来的结果 。

⚠️ 一个必须避开的错误说法: volatile(可见性关键字) 不等于「每次访问都绕过 CPU(处理器) 缓存、直接读写物理主存」,更 不能解释为「禁用 CPU(处理器) 缓存」 —— JavaGuide 特意强调了这一点。可见性说的是 happens-before(先行发生关系) 语义保证 ,不是缓存被关掉了。如果你在面试里说成「volatile(可见性关键字) 会强制读写主存」,很可能被追问到答不上来。

语义 2 · 禁止重排序(有序性)

如果字段被声明为 volatile(可见性关键字),实现必须保证相关写—读的内存顺序语义,不能进行破坏该语义的重排序。memory barrier(内存屏障)是理解实现的一种方式,具体指令不是本课的固定结论。

⚠️ 注意两处措辞精度:
① volatile(可见性关键字) 约束的是与该变量读写相关、可能破坏其内存语义的重排序 , 并不是禁止所有指令重排序优化 ;
② 常见的 4 类内存屏障(StoreStore(写写屏障) / StoreLoad(写读屏障) / LoadLoad(读读屏障) / LoadStore(读写屏障))是 便于理解的实现层模型 ,并非 JLS 规定 JVM(Java 虚拟机) 必须逐条插入哪些具体屏障 —— 实际 JVM(Java 虚拟机) 会按目标处理器的内存模型选择、合并或省略。

⚠️ 最关键的一点(也是面试最大的坑):volatile(可见性关键字) 不保证复合操作的原子性。
i++ 是「读—改—写」三步复合操作。即使 i 是 volatile(可见性关键字),两个线程同时执行还是会丢失更新。
要原子性,得用 synchronized(内置同步关键字)、Lock(显式锁接口) 或原子类(AtomicInteger(原子整数类),其底层是 CAS(比较并交换))。

「禁止重排序」有什么用?两个真实场景

场景 A · 状态标志位 (最简单的用法):一个线程写 volatile boolean stopped = true,另一个线程循环读它来决定是否退出。没有 volatile(可见性关键字),读线程可能永远看不到这个改动。

场景 B · 双重检查锁(DCL(双重检查锁定))单例 ——这是面试官最爱问的 volatile(可见性关键字) 应用题,代码如下(来自 JavaGuide,存档 reference/javaguide-concurrent-questions-02.md):

风险: 锁外快速读取必须有安全发布保证;示例用于解释同步关系,不用偶然运行结果替代规范证明。

public class Singleton {
    private volatile static Singleton uniqueInstance;   // ← volatile 在这里

    private Singleton() {}

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {                    // 第一次检查
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {            // 第二次检查
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

演示/验证: 这是初始化模式示例。核对首次检查在锁外、二次检查在锁内,以及引用通过 volatile(可见性关键字)发布;可编译并从两个调用处取值验证同一实例,但运行一次不证明所有内存模型行为。

为什么需要安全发布? 读取方第一次检查在锁外,可能取得对象引用后直接返回,未与创建方的解锁建立监视器关系。把引用声明为 volatile(可见性关键字),让构造期间的写入经“写引用 → 读引用”建立 happens-before(先行发生关系),再按程序顺序传递到对象字段读取。读到该引用后,发布前的初始化结果才有这里需要的保证。

“分配、初始化、发布”可用作推理分解,不能当作所有处理器固定的机器指令顺序。具体 memory barrier(内存屏障)取决于实现;本课不要求背某条屏障一定插在哪里。依据官方摘录D节的字段写—读规则与传递性。

💡 复述要点(修订): 「DCL(双重检查锁定) 的第一次检查在锁外,所以需要 volatile(可见性关键字) 安全发布对象 :构造期间的写入,经 volatile(可见性关键字) 引用的写—读建立 happens-before(先行发生关系),让读到该引用的线程看到初始化结果。」
这里既涉及相关重排序的约束,也涉及初始化结果的可见性;不能说成“不是可见性”。依据 JDK 的 volatile(可见性关键字) 规则与传递性(存档 官方摘录 D 节)。

四、检验:5 道题,点一下就知道对不对

先别看上面的内容,凭记忆作答 —— 这叫 检索练习 ,比再读一遍有效得多。

  1. volatile(可见性关键字) 关键字能保证以下哪一项?
  • A. 复合操作的原子性
  • B. 共享变量的可见性
  • C. 线程调度的公平性
  • D. 事务提交的持久性
查看答案与解释

答案:B

volatile(可见性关键字) 提供 可见性 和 禁止重排序 ,不让复合操作自动具备原子性。线程调度公平性是锁/调度器的话题,事务持久性是数据库的话题 —— 别被相似的词骗了。

  1. volatile(可见性关键字) 变量上的 i++ 为什么不安全?
  • A. 变量被缓存到本地内存
  • B. 写操作缺少了内存屏障
  • C. 自增本身包含多个步骤
  • D. 编译器取消了读取屏障
查看答案与解释

答案:C

i++ = 读取 → 加一 → 写回,三步之间可能被其他线程插入,导致丢失更新。关键是两次读写之间仍可能有别的更新;它不让整个自增成为一次原子操作。

  1. 双重检查锁(DCL(双重检查锁定))单例中,instance 为什么要加 volatile(可见性关键字)?
  • A. 保证实例只被创建一次
  • B. 保证对象被安全地发布
  • C. 提升多线程读取的性能
  • D. 防止反射创建多个实例
查看答案与解释

答案:B

第一次检查在锁外;volatile(可见性关键字)的写—读规则与传递性,让读到引用的一方看到发布前的初始化结果。它没有把整个创建流程自动变成一次原子操作。

  1. JMM(Java 内存模型) 里的「主内存」最准确的理解是?
  • A. 一块物理内存的硬件实体
  • B. 所有共享变量的规范抽象
  • C. 堆内对象所在新生代区域
  • D. 线程独享的活动栈帧空间
查看答案与解释

答案:B

JavaGuide 明确说:主内存是 规范层面的抽象,不等同于某一块物理内存 。同理「本地内存」也是说明线程访问行为的抽象,它涵盖缓存、写缓冲区、寄存器与编译器优化。

  1. 「JMM(Java 内存模型) 和 JVM(Java 虚拟机) 内存区域」的关系是?
  • A. 两者描述完全相同的内容
  • B. 两者描述不同方面的内容
  • C. 前者只是后者内部的子集
  • D. 后者只是前者底层的实现
查看答案与解释

答案:B

JVM(Java 虚拟机) 内存区域讲「运行时数据放哪」;JMM(Java 内存模型) 讲「并发时线程如何通过主内存通信」。这是不同层面的概念;一次答对不代表已经能解释全部内存问题。

五、把它讲出来:60 秒脱稿演练

面试不会让你做选择题,它会让你 开口讲 。先理解提纲,再合上这一页,用 60 秒讲清这个问题:

题目: 「volatile(可见性关键字) 的两个语义是什么?它解决什么问题?」

参考结构(记住顺序和关键词,不要背逐字稿):
① 先讲根源 —— CPU(处理器) 缓存 + 指令重排 → 可见性 / 有序性问题
② 再讲 JMM(Java 内存模型) —— 一组规范,主内存 + 本地内存的抽象
③ 两个语义 —— 可见性(写 happens-before(先行发生关系) 后续读)+ 禁止重排序
④ 补一句关键限定 —— 不保证复合操作的原子性 ,i++ 仍不安全
⑤ 给一个例子 —— DCL(双重检查锁定) 单例或状态标志位

自测要点:

  • 能独立解释可见性、相关重排序约束及复合操作不自动原子化
  • 能解释错误交错、建立具体先行发生关系,并处理改变条件后的追问
  • 能结合真实或明确标注的模拟场景说明选择、限制与验证依据;仅说出一个例子不算达到此档

练习: 把这段60秒回答记录成文字或录音,核对提纲后选出最需要修正的两三个问题,隔日再尝试一次。

六、面试官接着会追问什么(提前打个照面)

  1. volatile(可见性关键字) 能保证原子性吗?为什么?
  2. volatile(可见性关键字) 和 synchronized(内置同步关键字) 的区别?(提示:原子性 / 可见性 / 有序性 / 是否阻塞 / 性能)
  3. volatile(可见性关键字) 的底层实现原理是什么?(内存屏障:StoreStore(写写屏障) / StoreLoad(写读屏障) / LoadLoad(读读屏障) / LoadStore(读写屏障);注意这是便于理解的实现层模型,实际 JVM(Java 虚拟机) 会按处理器内存模型选择、合并或省略)
  4. 双重检查锁单例为什么必须加 volatile(可见性关键字)?不加会怎样?
  5. happens-before(先行发生关系) 是什么?volatile(可见性关键字) 变量规则怎么描述?
  6. JMM(Java 内存模型) 和 JVM(Java 虚拟机) 内存区域有什么区别?

这 6 个问题现在不用全答上来 —— 第 2、5 题在 第 4 课 展开,第 3 题的内存屏障本课第三节已经讲过。但你至少要知道 它们会被问 。

七、主来源

优先依据 Java 21 并发包的内存一致性说明,已存档于官方摘录;以下学习资料用于解释和示例,规范保证以官方来源为准。

下一课: 第 4 课 synchronized(内置同步关键字)(原理、2026 版锁升级、happens-before(先行发生关系) 常用规则)。

⚠️ 2026 时效提醒(已在备课中核查): 大量中文面试资料仍把「偏向锁」当作锁升级的一环,但 偏向锁自 JDK 15(JEP 374)起默认禁用并标记为废弃,JDK 18 彻底移除 ;到 JDK 21,带着偏向锁启动参数的 JVM(Java 虚拟机) 会直接启动失败(依据:JavaGuide 转述 JEP 374、本机 JDK 8–25 实测,见 实验记录(本地资料);OpenJDK 记录 JDK-8231264、JDK-8256425 待联网核对)。本系列以 Java 17/21 为例,区分历史优化与现代实现。

相关: 第 4 课 synchronized(内置同步关键字) · 并发速查表 · 学习导读:通用学习路线 · 学习路线速查 · 学习导读:Java 知识地图