volatile 与 JMM 内存模型

共 19 题
📑 题目列表 19 题
#
★★★

1. JMM 1.0(JSR 133)内存模型的影响范围

JMM 1.0(JSR 133)内存模型的影响范围是什么?

  • JSR 133 的背景
  • final 字段语义
  • volatile 语义修正

JSR 133 是 Java 5(JDK 1.5)确立的 Java 内存模型(JMM 1.0),修正了旧模型(JSR 133 之前)的问题。影响范围:1) 重新定义 volatile 语义(更强的可见性与有序性,禁止更多重排);2) 引入 final 字段的安全发布保证(构造器内 final 字段对其他线程可见且不受重排影响);3) 完善 happens-before 规则与同步语义;4) 解决了 double-checked locking 的合法性问题(需 volatile);5) 明确各种同步操作(锁、volatile、final)的可见性推导。它奠定了 Java 并发正确性的理论基础。

JSR 133 是"并发正确性"的宪法,影响所有 Java 并发编程。它让某些以前"碰巧正确"的写法变成"规范保证正确"。

#
★★★

2. JMM 与 CPU 缓存一致性(MESI)协议的关系

JMM 与 CPU 缓存一致性(MESI)协议的关系是什么?

  • MESI 的硬件一致性
  • JMM 的软件抽象
  • 多级缓存与可见性

MESI 是 CPU 缓存一致性协议,保证多个核的缓存对同一缓存行的读写一致(Modified/Exclusive/Shared/Invalid 四种状态),是硬件层面的可见性保证。JMM 是 Java 语言层面的抽象,比硬件更"宽松":JMM 允许编译器/CPU 重排(只要不违反 happens-before),并提供 volatile、锁等抽象来显式控制可见性与排序。关系:JMM 的可见性最终由硬件缓存一致性(MESI)支撑,但 JMM 不依赖具体 MESI 细节,而是通过内存屏障(barrier)映射到底层协议。JMM 是"内存模型抽象",MESI 是"具体实现之一"。

分层理解:MESI 管"缓存同步",JMM 管"语言级规则"。JMM 用屏障把抽象语义映射到硬件(如 x86 的 lock 前缀、ARM 的 dmb)。

#
★★★

3. JMM 在 CPU 缓存一致性协议(MESI)下的抽象映射细节

JMM 在 CPU 缓存一致性协议(MESI)下的抽象映射细节是什么?

  • 缓存行与 MESI 状态
  • volatile 写与屏障
  • 抽象映射

JMM 的内存可见性最终依赖 CPU 缓存一致性。映射细节:volatile 写会触发"立即写回主存并使其他缓存失效"(对应 MESI 的 Modified→Shared 流程),通过 store barrier 防止重排;volatile 读会"失效本地缓存并重新加载"(对应 Invalid 状态读取),通过 load barrier 保证读到最新。JMM 的 acquire/release 语义映射到 load-acquire/store-release 屏障。JMM 起着"遮蔽不同硬件差异"的作用:同一段 Java 代码在不同 CPU(x86/ARM)上由 JVM 插入合适的屏障,保证一致的 JMM 语义。

JMM 是"可移植抽象",屏障是"具体落地"。同一 JMM 语义在 x86(TSO 强序)与 ARM(弱序)插入不同数量的屏障。

#
★★★

4. JMM 形式化(Java Memory Model Formalism)

JMM 形式化(Java Memory Model Formalism)是什么?

  • 形式化模型
  • 因果关系与执行轨迹
  • 弱/强行为的推导

JMM 形式化是用数学语言定义"哪些执行轨迹(execution)是合法的、哪些值可以被读到"。JMM 通过"因果关系(causality)+ happens-before + 同步顺序"定义:一个执行合法当且仅当存在一个满足 happens-before 偏序与 causality 约束的执行轨迹,且每个读到的值要么是默认值(0/null),要么是某个写所写入的值。形式化模型允许"弱行为"(如 relaxed 读到的旧值只在特定条件下合法),并禁止 out-of-thin-air 值。它是判断"某并发代码是否有相应行为"的理论依据。

JMM 形式化回答"什么重排/读值合法"。它比直觉更严格,是 JSR 133 规范的核心,也是 jcstress 等工具验证的依据。

#
★★★

5. JMM(Java Memory Model)的 happens-before 规则在虚拟线程间的可见性保证如何

JMM 的 happens-before 规则在虚拟线程间的可见性保证如何?

  • JMM 不区分线程类型
  • 虚拟线程的 happens-before
  • 与平台线程一致

JMM 的 happens-before 规则对虚拟线程与平台线程一视同仁:虚拟线程之间、虚拟线程与平台线程之间,通过同步操作(锁、volatile、final、线程启动/join)建立 happens-before 关系,可见性保证一致。虚拟线程只是调度实现不同(载体线程池),但 JMM 语义不变。因此,虚拟线程间共享可变数据同样需要正确的同步(volatile/锁/原子类),不能因为"虚拟线程"就认为天然安全。JMM 的抽象不感知线程是虚拟还是平台。

JMM 是"线程无关"的抽象。虚拟线程的可见性语义与平台线程完全一致,只是并发调度模型不同。

#
★★★

6. JMM(Java Memory Model)的核心概念与 happens-before 原则

JMM(Java Memory Model)的核心概念与 happens-before 原则是什么?

  • 原子性、可见性、有序性
  • happens-before 规则
  • 各种同步操作

JMM 核心概念:原子性(操作不可分割)、可见性(一个线程的写对其他线程可见)、有序性(指令重排有约束)。happens-before 原则:若操作 A happens-before 操作 B,则 A 的写对 B 可见且 A 在 B 之前执行。主要规则:程序顺序规则(同一线程内按程序顺序)、监视器锁规则(解锁 happens-before 后续加锁)、volatile 规则(volatile 写 happens-before 后续读)、线程启动/join 规则、传递性。这些规则是推导并发正确性的基础。

happens-before 是 JMM 的灵魂,它把"可见性"转化为"可推导的偏序关系"。理解它就能分析任意并发代码。

#
★★★

7. double-checked locking 在 JDK 5+ 的正确实现与 volatile 关键作用

double-checked locking 在 JDK 5+ 的正确实现是什么?volatile 的关键作用是什么?

  • DCL 的写法
  • volatile 禁止重排
  • 半初始化对象问题

DCL 正确实现:单例字段用 volatile,第一次检查是否为 null(避免进入锁),加锁后第二次检查(防止重复初始化),构造后用 volatile 语义保证安全发布。volatile 的关键作用:禁止"构造对象"与"赋值引用"之间的重排。若非 volatile,可能先赋值引用(指向未构造完成的对象)再构造,其他线程读到半初始化对象。volatile 保证构造完成后才发布引用,且可见性正确。

DCL 的脆弱点在于对象发布的瞬间。volatile 用"抑制重排 + 安全发布"解决半初始化问题。JDK 5+ 合法。

private static volatile Singleton instance;
public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) instance = new Singleton();
        }
    }
    return instance;
}
#
★★★

8. final 字段在 JMM 中的安全发布保证

final 字段在 JMM 中提供什么安全发布保证?

  • final 字段的特殊语义
  • 构造器结束点
  • 安全发布

JMM 对 final 字段提供特殊保证:final 字段在构造器结束处建立"冻结"语义,构造完成后,其他线程读取该 final 字段时,能保证看到构造器内写入的最终值(不受重排影响),且 final 字段引用的对象(若 final 引用)的字段也可见。这使 final 字段构成"安全发布":无需 volatile 或锁,构造完成后即可安全发布。但注意:若 this 引用逃逸(构造器内泄露 this),final 保证失效。

final 字段是"安全发布"的轻量手段,靠构造器冻结语义。它是不可变对象线程安全的基础。

#
★★

9. happens-before 与 synchronized 块边界的等价

happens-before 与 synchronized 块边界的等价关系是什么?

  • 锁的获取/释放
  • 块边界
  • 可见性

synchronized 块边界建立 happens-before:进入块(获取锁)与退出块(释放锁)之间,释放锁 happens-before 后续任何线程获取同一把锁。因此,线程 A 在 synchronized 块内写的数据,线程 B 在获取同一锁后必然可见(A 释放锁 happens-before B 获取锁)。这等价于"临界区内的写对后续持锁者可见"。理解边界:锁释放是"发布点",锁获取是"获取点",二者配对形成可见性链。

synchronized 的可见性 = 释放锁(发布)与获取锁(获取)间的 happens-before。这是"锁能保证可见性"的依据。

#
★★

10. release 写入与 acquire 读取如何建立可见性链,和完整 volatile 读写相比允许哪些重排序

release 写入与 acquire 读取如何建立可见性链?和完整 volatile 读写相比允许哪些重排序?

  • release/acquire 配对
  • 可见性链
  • 与 volatile 的重排差异

release 写与 acquire 读配对建立可见性链:线程 A 做 release 写(和之前的所有写),线程 B 对同一变量做 acquire 读,则 A release 之前的所有写对 B 可见(B 的 acquire 读之后)。与 volatile 相比,release/acquire 允许更多重排:release 写允许其后的操作被重排到写之前(但之前的不行),acquire 读允许其前的操作被重排到读之后(但之后的不行),且两者都不提供 volatile 的全序(total order)。因此 release/acquire 更弱但更省(少屏障)。

release/acquire 是"单向约束",只保证发布与获取方向,不保证全序,故比 volatile 允许更多重排、开销更低。

#
★★

11. volatile 关键字的语义(可见性、禁止重排序)

volatile 关键字的语义是什么(可见性、禁止重排序)?

  • 可见性
  • 禁止重排序
  • 原子性边界

volatile 提供:1) 可见性:volatile 写对其他线程立即可见(写回主存并失效缓存),volatile 读读到最新值;2) 有序性:禁止指令重排(volatile 写之前/之后的操作不能跨越写被重排,建立内存屏障),且 volatile 写 happens-before 后续 volatile 读。但它不保证原子性:volatile 的复合操作(如 i++)不是原子的,需原子类或锁。volatile 适合"状态标志、发布不可变引用"等单次读写场景。

volatile = 可见性 + 有序性(无原子性)。理解"能保证什么、不能保证什么"是掌握 volatile 的关键。

#
★★

12. volatile 写操作的内存屏障(Memory Barrier)在 x86 与 ARM 架构下的成本差异

volatile 写操作的内存屏障在 x86 与 ARM 架构下的成本差异是什么?

  • x86 的 TSO 强序
  • ARM 的弱序
  • 屏障成本

x86 采用 TSO(Total Store Order)强序模型,硬件本身有序,volatile 写通常只需一个 store 屏障(甚至可复用已有屏障),成本低。ARM 是弱序模型,硬件允许大量重排,volatile 写需要显式的 dmb/dsb 屏障(store barrier + 可能 full barrier),成本高。因此同一 volatile 写代码在 ARM 上比 x86 开销大。JVM 根据架构插入不同的屏障:x86 少、ARM 多,保证一致的 JMM 语义。这也是并发代码在弱序架构上更容易出问题的原因。

屏障成本 = 架构内存序强度。x86 强序省屏障,ARM 弱序需更多屏障,成本差异显著。

#
★★

13. volatile 数组与单个元素的可见性差异

volatile 数组与单个元素的可见性差异是什么?

  • volatile 数组引用
  • 元素可见性
  • 需求用原子数组

volatile 修饰的是数组引用(数组对象本身),不修饰数组元素。因此 volatile 数组只能保证"引用"的可见性,不能保证"元素"的可见性——元素读写仍是普通操作,可能被重排或读到旧值。若要保证数组元素的原子性与可见性,需用 AtomicIntegerArray/AtomicReferenceArray 或 VarHandle 对元素做原子/volatile 访问。这是 volatile 数组的常见陷阱。

volatile 数组 = volatile 引用,元素无 volatile 语义。需元素级可见性时用原子数组类。

#
★★

14. 单例模式双重检查锁(DCL)的 volatile 必要性

单例模式双重检查锁(DCL)为什么必须用 volatile?

  • 半初始化对象
  • 重排
  • volatile 必要性

DCL 中单例字段必须是 volatile,否则不安全。原因:new Singleton() 可分为"分配内存→构造→赋值引用"三步,编译器/CPU 可能把"赋值引用"重排到"构造"之前。若不用 volatile,另一个线程可能读到已赋值的引用但对象尚未构造完成(半初始化),导致使用空/错误对象。volatile 禁止该重排,保证构造完成后才发布引用,从而安全。这是 DCL 正确性的关键。

DCL 的 bug 根源是引用发布与构造的乱序。volatile 用"抑制重排"解决,是 JDK 5+ 正确 DCL 的必要条件。

#
★★

15. 成功 release 后对后继线程的可见性由哪些内存语义保证,自定义同步器最容易漏掉什么

成功 release 后对后继线程的可见性由哪些内存语义保证?自定义同步器最容易漏掉什么?

  • release 的发布语义
  • 后继获取的可见性
  • 自定义同步器易漏点

成功 release 后,同步器会通过 AQS 的 state 更新(volatile 写)与 unpark 唤醒后继,后继线程获取锁时 acquire(volatile 读)建立 happens-before:释放者临界区内的所有写对后继获取者可见。自定义同步器最容易漏掉的是:1) 忘记在 release 时正确更新 state 为 volatile(AQS 用 volatile state,但自定义若用普通字段则无可见性);2) 释放后未正确唤醒后继(未 unpark 导致后继饿死);3) 未保证"释放前写的数据在获取后可见"(需依赖 state 的 volatile 语义)。核心是确保 state 的 volatile 读写与唤醒配对。

可见性依赖"volatile state 写(release)+ volatile state 读(acquire)的 happens-before"。漏掉 volatile 或唤醒是常见错误。

#
★★

16. 指令重排序(编译器、CPU、内存)的真实案例

指令重排序(编译器、CPU、内存)的真实案例是什么?

  • 编译器重排
  • CPU 乱序执行
  • 内存重排案例

典型案例:无 volatile 或同步时,以下代码可能输出异常结果(如 0 0):

int x=0,y=0,a=0,b=0;
// 线程1: x=1; r1=b;        线程2: y=1; r2=a;

因编译器/CPU 重排,可能出现 r1=0 且 r2=0(两个线程的写被重排到读之后),违反直觉。根源:编译器指令重排、CPU 乱序执行、内存系统重排(Store Buffer 延迟写可见)。只有 volatile/synchronized/原子操作能建立屏障阻止。这说明并发代码必须显式同步,不能依赖"直觉顺序"。

重排是真实存在的,且多层叠加。这个案例是"为什么需要内存屏障"的经典证明。

#
★★

17. JDK 25 中 MemorySegment(外部内存 API)对堆外内存的可见性保证

JDK 25 中 MemorySegment(外部内存 API)对堆外内存提供什么可见性保证?

  • MemorySegment 的并发访问
  • 可见性/同步
  • 与堆内存对比

MemorySegment(JEP 454 外部内存 API)允许安全地访问堆外内存,支持并发访问(可按 Segment 隔离、线程限制)。其可见性保证:MemorySegment 的访问默认不提供语言级 happens-before,除非通过同步机制(锁、volatile、原子操作)或显式 memory ordering(如 VarHandle 的 volatile/acquire/release 访问段内元素)。也就是说,堆外内存的并发可见性需要开发者用同步或 VarHandle 内存序保证,与普通字段一致。它不自动提供跨线程可见性。

MemorySegment 提供的是"安全访问"而非"自动可见性"。跨线程共享堆外内存同样需同步或显式内存序。

#

18. Java 25 内存模型与 Vector API(JEP 508)的内存序

Java 25 内存模型与 Vector API(JEP 508)的内存序是什么?

  • Vector API 的并发
  • 内存序
  • 与 JMM 关系

Vector API(JEP 508)提供 SIMD 向量计算,其操作面向数据并行,不直接暴露复杂内存序。对内存数组的访问,Vector API 通过 MemorySegment + VarHandle 的访问控制内存序;向量运算本身不引入新的内存模型。向量读写的可见性遵循宿主内存的 JMM 语义(用 VarHandle 的 volatile/acquire/release 控制)。开发者按需选择内存序,默认 plain 访问无额外保证。Vector API 不改变 JMM 本身,只是内存访问的内存序由调用方用 VarHandle 指定。

Vector API 是计算并行,内存序仍由 VarHandle/Segment 控制,不偏离 JMM。它不新增内存模型规则。

#

19. Thread.yield() 与内存可见性

Thread.yield() 与内存可见性有什么关系?

  • yield 的语义
  • 是否提供可见性
  • 与同步对比

Thread.yield() 只是提示当前线程让出 CPU 执行权(进入 RUNNABLE 队列,可能立即被调度回),它不提供任何内存可见性保证,也不建立 happens-before,不刷新缓存、不确保其他线程看到最新值。因此不能把 yield 当作同步手段。可见性必须靠 volatile/synchronized/原子操作/JMM 规则。yield 仅用于"忙等时让出 CPU"或调度提示,与内存模型无关。

常见误区是认为 yield 能"刷新内存"。实际 yield 只影响调度,不处理可见性。正确的同步靠锁/volatile。