专栏文章
专栏文章
计算机原理专栏
1. 计算机原理专栏 #01:CPU 与指令执行 2. 计算机原理专栏 #02:存储体系 3. 计算机原理专栏 #03:IO 与总线 4. 计算机原理专栏 #04:内存模型

计算机原理专栏 #04:内存模型

发布于 2026-06-05 06:27 👁 32 次阅读
#并发#java#computer-architecture#jmm#memory-model

内存模型

本文从硬件层的 CPU 内存模型出发,逐层深入到 Java 内存模型(JMM),系统讲解内存可见性、重排序、happens-before、volatile、synchronized 的底层原理,以及各层次之间的关联。是理解 Java 并发编程|Java 并发编程 的必备基础。


目录

章节 说明
概念名词解释 全文高频术语速查
为什么需要内存模型 多核 + 优化带来的混乱
CPU 内存模型 强模型 vs 弱模型,x86 TSO,ARM
内存重排序的四种类型 编译器重排 + CPU 乱序执行
内存屏障 硬件层的解决方案
Java 内存模型(JMM) 抽象内存模型,主内存与工作内存
happens-before 规则 JMM 的核心承诺
volatile 深度解析 可见性 + 禁止重排序的完整实现
synchronized 与内存模型 获取/释放语义
final 与安全发布 初始化安全性
双重检查锁定(DCL) 经典陷阱与正确写法
各层对照总结 硬件 → JVM → Java 的映射

概念名词解释

硬件 / CPU 层

术语 简释
Load / Store(内存读写操作) 概念层术语,指 CPU 对内存地址的读 / 写请求(x86 上由 MOV 指令承担,无单独 LOAD/STORE 指令名,见 Intel SDM Vol.2 — MOV)。
数据路径:读(Load)= 内存地址 → Cache 层次(L3/L2/L1)→ 寄存器;写(Store)= 寄存器 → Store Buffer →(Retire 时)L1 Cache → L2/L3 → 主内存。
跨核可见性:写在 Store Buffer 阶段对他核完全不可见("cannot see the write queues on other processors",来源:research.swtch.com/hwmm);当 Store Buffer flush 到 Cache 的那一刻,MESI 总线嗅探(Bus Snooping)机制通知其他核使其副本失效,他核的后续读才能获得新值(来源:Wikipedia — MESI protocol)。寄存器是每个核私有的落脚点,不参与跨核共享。LoadLoad / StoreLoad 等内存屏障命名均以此为基础。⚠️ 与 JMM 原子操作中的 load / store 同名但含义不同,见下方 JMM 层
Store Buffer(存储缓冲区) 缓冲已发射(Issue)但尚未退休(Retire)的写操作,即"飞行中"的 Store 指令。数据在 Retire 时才写入 L1 Cache,Retire 之前对其他核完全不可见(Store-Load 重排序的根源);但本核可以读取自己的 Store Buffer(Store-to-Load 转发),其他核不行。来源:research.swtch.com/hwmmWikipedia — Memory Disambiguation
Load Buffer(加载缓冲区) 缓冲已发射但尚未退休的读操作,与 Store Buffer 对称,同为飞行中指令的缓冲器。Load 发射后在此处登记,执行时读 Cache(或由 Store Buffer 转发数据),Retire 时数据提交到架构寄存器。⚠️ 不是数据线性流过的通路(数据从 Cache 直接返回,不经 Load Buffer 中转);核心用途:① 追踪飞行中的 Load;② 被 Store 搜索以检测 RAW 违规(Memory Disambiguation)。来源:Wikipedia — Memory Disambiguation
Cache(CPU 高速缓存) 位于 CPU 与主内存之间的多级缓存(L1/L2/L3);读写速度远快于内存,但多核间存在副本不一致问题
Invalidate Queue(失效队列) 收到其他核发来的"请失效该缓存行"消息后,先放入此队列异步处理;是 Load 延迟感知他核写入的原因
MESI 协议 四状态缓存一致性协议:Modified(已修改)/ Exclusive(独占)/ Shared(共享)/ Invalid(已失效);保证多核 Cache 最终一致
Out-of-Order Execution(乱序执行) CPU 在不违反数据依赖的前提下,动态调整指令执行顺序以填满流水线空泡;对单线程透明,但多线程可见
TSO(Total Store Order) x86 的内存模型;仅允许 Store-Load 一种重排(Store Buffer 导致),其余三种顺序均由硬件保证
内存屏障(Memory Barrier / Fence) CPU 提供的特殊指令,强制约束其前后内存操作的执行顺序,并可冲刷 Store Buffer;分 LoadLoad / StoreStore / LoadStore / StoreLoad 四类
Acquire 语义 屏障之后的所有读写,不能被重排到该操作之前;对应"进入临界区,先看到最新数据"
Release 语义 屏障之前的所有读写,不能被重排到该操作之后;对应"离开临界区,先把修改刷出去"

"飞行中"(in-flight)补充说明:Load Buffer 和 Store Buffer 都是缓冲飞行中操作的结构,称其为"缓冲器"完全准确。"飞行中"指指令已从发射队列(Issue Queue)发出、但尚未退休(Retire)——退休是指令按程序顺序提交、结果真正生效的时刻。两者的关键区别:Store Buffer 里的写尚未写入 Cache(Retire 时才写),对其他核不可见;Load Buffer 里的读已经发出了 Cache 访问请求(或正在等 Cache 返回数据),等 Retire 后才提交到寄存器。两者都不是数据线性流经的通路,而是乱序执行引擎用来追踪飞行中操作状态的结构。

JMM 层

术语 简释
JMM(Java Memory Model) Java 语言规范(JLS §17.4)定义的抽象内存模型;屏蔽硬件差异,统一规定可见性与重排序规则
主内存(Main Memory) JMM 中所有线程共享变量的逻辑存储区域;对应硬件的 Cache + RAM
工作内存(Working Memory) 每个线程私有的变量副本;对应硬件的寄存器、Store Buffer、L1 Cache
内存可见性(Visibility) 一个线程对共享变量的写,何时能被其他线程的读所感知
重排序(Reordering) 编译器或 CPU 改变指令执行顺序;在单线程内结果等价,但可能破坏多线程程序的正确性
数据竞争(Data Race) 两个线程并发访问同一变量、至少一个为写、且没有同步手段;行为未定义
顺序一致性(Sequential Consistency) 理想模型:所有操作按某个全局顺序执行,且每个线程内顺序与代码一致;JMM 仅对正确同步的程序提供此保证
happens-before JMM 的核心可见性关系:若 A happens-before B,则 A 的结果对 B 可见、A 在 B 之前执行;是推理并发正确性的基础工具
8 种原子操作(lock / unlock / read / load / use / assign / store / write) JLS 第二版 §17.1(旧版 JMM,适用于 Java 1.0–1.4;Java 5 起被 happens-before 模型取代)定义的线程与内存交互的最小原语,详见 8 种原子操作详解。⚠️ 其中 load / store 与硬件 Load / Store 同名不同义(硬件 Load / Store 的准确数据路径见上方 Load / Store(内存读写操作) 条目):JMM load = 将 read 传来的值装入工作内存副本;JMM store = 将工作内存副本的值取出准备传给主内存

Java 语言层

术语 简释
volatile 关键字;保证变量读写的可见性,并禁止与前后操作发生重排序;不保证复合操作(如 i++)的原子性
synchronized 关键字;提供互斥(同一时刻只有一个线程执行临界区)+ 内存可见性(进入时刷新,退出时写回)
final 关键字;JMM 保证对象构造完成后 final 字段对所有线程可见,无需额外同步;前提是构造函数中不能泄露 this
monitorenter / monitorexit synchronized 编译后的字节码指令;分别对应 Acquire 屏障(进入临界区)和 Release 屏障(退出临界区)

为什么需要内存模型

三大破坏因素

单线程程序里,代码按顺序执行,结果可预测。多核并发场景下,有三股力量会打乱这种直觉:

1. 编译器重排序   — 编译期为优化生成的指令顺序与源码不一致
2. CPU 乱序执行   — CPU 动态调整指令执行顺序(Out-of-Order Execution)
3. 存储缓冲区     — Store Buffer / Invalidate Queue 导致写操作延迟可见

经典问题:

// 初始值: x = 0, y = 0, flag = false

// 线程 A
x = 42;
flag = true;   // 编译器/CPU 可能把这行提前到 x=42 之前

// 线程 B
while (!flag) {}
print(x);      // 可能打印 0,不是 42!

这就是内存可见性问题的典型表现:线程 A 写的值,线程 B 读不到最新版本。

内存模型的定义

内存模型(Memory Model)是一套规范,定义:

有了内存模型,程序员才能推理多线程程序的正确性。


CPU 内存模型

强模型 vs 弱模型

不同 CPU 架构对内存操作重排序的宽松程度不同:

架构 内存模型 允许的重排序
x86 / x86-64 TSO(Total Store Order) 仅 Store-Load 重排(Store Buffer 导致)
ARM / AArch64 弱模型(Relaxed) 几乎所有重排序都允许
RISC-V 弱模型 类似 ARM,需显式 FENCE
SPARC (TSO 模式) TSO 同 x86
PowerPC 弱模型 允许大量重排序

规律:x86 是现代主流服务器/桌面 CPU,内存模型较强;ARM 是移动端/新型服务器(如苹果 M 系列)的主流,内存模型弱,需要更多显式屏障。

x86 TSO 模型详解

TSO(Total Store Order)的核心规则:

Load  → Load   ✅ 保序(后面的 Load 不会越过前面的 Load)
Store → Store  ✅ 保序(后面的 Store 不会越过前面的 Store)
Load  → Store  ✅ 保序(Load 不会越过后面的 Store)
Store → Load   ❌ 允许重排(Store Buffer 导致)

唯一允许的重排:Store 越过后续 Load。

这是因为 存储体系|Store Buffer 的存在:写操作先放入 Store Buffer,CPU 继续执行后续读操作,Store Buffer 异步刷入 Cache(多核间通过 存储体系|MESI 协议 保持缓存一致性)。其他核在 Store 刷入 Cache 之前,读到的是旧值。

Core A:                         Core B:
x = 1  → Store Buffer(未刷)    y = 1  → Store Buffer(未刷)
r1 = y → 从 Cache 读,得到 0      r2 = x → 从 Cache 读,得到 0

结果:r1=0, r2=0  ← 在 x86 上实际可能发生!

ARM 弱内存模型

ARM 允许几乎所有重排,包括:

这意味着在 ARM 上写正确的多线程代码,需要大量显式屏障指令,Java 程序员通常不需要关心(JVM 负责插入),但在理解 JVM 实现时很关键。


内存重排序的四种类型

站在 Java 程序员视角,重排序分四类:

LoadLoad   重排序:Load1 → Load2  变为  Load2 → Load1
StoreStore 重排序:Store1 → Store2 变为  Store2 → Store1
LoadStore  重排序:Load1 → Store2 变为  Store2 → Load1
StoreLoad  重排序:Store1 → Load2 变为  Load2 → Store1  ← 危害最大

StoreLoad 重排序危害最大:Core A 写地址 X(停在 Store Buffer 未刷出),随即读不同地址 Y(直接从 Cache 取旧值)。对其他核的观察者来看,Core A 的读 Y 发生在写 X 之前——即使程序顺序是先写后读。本质是跨核的顺序可见性问题,而非"本核看不见自己的写"——同地址写后读由 Store-to-Load Forwarding 保证,本核永远能看到自己的写。

各类型经典示例(x86 vs ARM)

用双线程 litmus test 演示四种重排序,并对比两大主流平台的行为。 来源:Weak vs. Strong Memory Models — Jeff PreshingHardware Memory Models — Russ Cox

StoreLoad — Store Buffering(SB)

初始: x = 0, y = 0

Core A:                          Core B:
x = 1   → Store Buffer(未刷)    y = 1   → Store Buffer(未刷)
r1 = y  → 从 Cache 读,得到 0     r2 = x  → 从 Cache 读,得到 0

结果 r1=0, r2=0:
  x86(TSO)  ✅ 可能发生  ← Store Buffer 导致,TSO 唯一允许的重排
  ARM(弱模型)✅ 可能发生

两核的写各自停在 Store Buffer 中,彼此的读看不到对方未刷出的写。

StoreStore — Message Passing 写者侧(MP)

初始: x = 0, flag = 0

Core A(写者):                  Core B(读者):
x = 42   ←─┐ StoreStore 重排     r1 = flag   // 自旋直到读到 1
flag = 1 ←─┘ flag=1 先写出        r2 = x      // ARM 上可能读到 0 !

结果 r1=1, r2=0:(flag=1 已可见,但 x=42 尚未写出)
  x86(TSO)  ❌ 不会发生  ← TSO 保证同核写操作的可见顺序
  ARM(弱模型)✅ 可能发生

修复:在 x=42flag=1 之间插入 StoreStore 屏障(ARM:DMB ISH)。x86 TSO 已在硬件层面保证 StoreStore 顺序,无需额外屏障(SFENCE 仅用于 Non-Temporal Store,如 MOVNTI)。

LoadLoad — Message Passing 读者侧(MP)

初始: x = 0, flag = 0
(Core A 已用 StoreStore 屏障保证 x=42 先于 flag=1 写出)

Core B(读者):
r1 = flag  // 读到 1(flag 的写已传播)
r2 = x     // LoadLoad 重排:ARM 上可能仍读到 0 !
           // Invalidate Queue 中对 x 的失效通知还未处理

结果 r1=1, r2=0:(看到了 flag=1,但 x 的失效还堵在队列里)
  x86(TSO)  ❌ 不会发生  ← TSO 保证同核读操作的顺序
  ARM(弱模型)✅ 可能发生

修复:在两次读之间插入 LoadLoad 屏障(ARM:DMB ISH)。

LoadStore — Load Buffering(LB)

初始: x = 0, y = 0

Core A:                          Core B:
r1 = x   ← Load                  r2 = y   ← Load
y = 1    ← Store                 x = 1    ← Store

LoadStore 重排后:Store 先于本核的 Load 发出
Core A:y=1 先写出,r1=x 后读(此时 Core B 的 x=1 已写出)→ r1=1
Core B:x=1 先写出,r2=y 后读(此时 Core A 的 y=1 已写出)→ r2=1

结果 r1=1, r2=1:
  x86(TSO)  ❌ 不会发生  ← TSO 禁止 LoadStore 重排
  ARM(弱模型)✅ 理论上允许,实际极罕见

两核的 Store 提前写出,后续 Load 各自读到了对方的 Store 结果,形成"循环可见"。

平台对比速查

重排序类型 典型 litmus test 根因 x86(TSO) ARM(弱模型)
StoreLoad Store Buffering Store Buffer 未刷 ✅ 可能 ✅ 可能
StoreStore Message Passing 写者 写可见顺序乱序 ❌ 不会 ✅ 可能
LoadLoad Message Passing 读者 Invalidate Queue 延迟 ❌ 不会 ✅ 可能
LoadStore Load Buffering Store 先于 Load 写出 ❌ 不会 ✅ 理论可能

编译器重排序 vs CPU 重排序

来源 发生时机 解决方式
编译器重排序 编译期,javac/JIT 优化 编译器屏障(禁止指令移动)
CPU 与指令执行|CPU 乱序执行 运行期,CPU 流水线调度 内存屏障指令
Store Buffer 运行期,写操作延迟可见 Store 屏障(冲刷 Store Buffer)
Invalidate Queue 运行期,失效通知延迟处理 Load 屏障(处理 Invalidate Queue)

内存屏障

内存屏障(Memory Barrier / Memory Fence)是 CPU 提供的特殊指令,强制约束其前后的内存操作顺序。

四种屏障语义

屏障类型 禁止的重排序 作用
LoadLoad Barrier Load1 不能越过 Load2 确保 Load1 的结果在 Load2 之前可见
StoreStore Barrier Store1 不能越过 Store2 确保 Store1 在 Store2 之前写出
LoadStore Barrier Load 不能越过后续 Store 确保读操作在后续写之前完成
StoreLoad Barrier Store 不能越过后续 Load 最强屏障,冲刷 Store Buffer,代价最高

x86 上的实现

MFENCE   ; 全屏障 = StoreLoad + 冲刷 Store Buffer
LFENCE   ; Load 屏障 = LoadLoad(x86 上通常是 no-op)
SFENCE   ; Store 屏障 = StoreStore(x86 上通常是 no-op)

实践:x86 上 JVM 的 volatile 写使用 LOCK ADD $0, (%rsp)MFENCE 充当全屏障;由于 TSO 模型已经保证了大多数顺序,只有 StoreLoad 需要显式屏障。

ARM 上的实现

DMB ISH   ; Data Memory Barrier, Inner Shareable Domain
          ; 全屏障:barrier 前的访问对其他核可见后,才执行 barrier 后的访问
DSB ISH   ; Data Synchronization Barrier,更强,还等待 Cache/TLB 操作完成
ISB       ; Instruction Synchronization Barrier,冲刷流水线

ARM 需要大量 DMB 指令,这是为什么 Java volatile 在 ARM 上性能开销明显高于 x86 的原因。

各类型重排序的修复方案

对应前文四个 litmus test,以伪汇编展示屏障插入位置。

StoreLoad — Store Buffering(SB)

两核都需要插入屏障,仅一侧加无效。

; Core A                        ; Core B
MOV [x], 1                      MOV [y], 1
MFENCE    ← x86                 MFENCE    ← x86
DMB ISH   ← ARM                 DMB ISH   ← ARM
MOV r1, [y]                     MOV r2, [x]

屏障冲刷 Store Buffer,使 x=1 / y=1 在后续读执行前对所有核可见, 排除 r1=0, r2=0 的结果。

StoreStore — Message Passing 写者侧(MP)

只需在写者的两次写之间插入屏障。

; Core A(写者)
MOV [x], 42
; MFENCE 不需要 ← x86 TSO 已保证 StoreStore 顺序
DMB ISH             ← ARM
MOV [flag], 1

屏障确保 x=42flag=1 对外可见之前已写出,读者看到 flag=1x 一定是 42。

LoadLoad — Message Passing 读者侧(MP)

只需在读者的两次读之间插入屏障。

; Core B(读者)
SPIN:  MOV r1, [flag]
       TEST r1, r1
       JZ   SPIN           ; 自旋直到 flag=1
; LFENCE 不需要 ← x86 TSO 已保证 LoadLoad 顺序
DMB ISH                     ← ARM
       MOV r2, [x]          ; 有屏障后,此时 x 一定是 42

屏障清空 Invalidate Queue,确保读 flag=1 之后,x 的失效通知已处理完毕。

LoadStore — Load Buffering(LB)

两核各自在 Load 与 Store 之间插入屏障。

; Core A                        ; Core B
MOV r1, [x]                     MOV r2, [y]
; 不需要 ← x86 TSO 已保证       ; 不需要 ← x86 TSO 已保证
DMB ISH   ← ARM                 DMB ISH   ← ARM
MOV [y], 1                      MOV [x], 1

屏障阻止 Store 先于 Load 写出,消除 r1=1, r2=1 的"循环可见"。

汇总对比

重排序类型 屏障插入位置 x86 是否需要 ARM 指令
StoreLoad Store 与 Load 之间(两侧各插) MFENCE DMB ISH
StoreStore 两次 Store 之间(写者侧) ❌ TSO 已保证 DMB ISH
LoadLoad 两次 Load 之间(读者侧) ❌ TSO 已保证 DMB ISH
LoadStore Load 与 Store 之间(两侧各插) ❌ TSO 已保证 DMB ISH

x86 只有 StoreLoad 需要显式屏障,这正是 TSO(Total Store Order)名称的由来——除 Store-Load 外,其余三种顺序由硬件总序保证。

Acquire / Release 语义

这是一对重要的屏障语义,常见于 Lock/Unlock 场景:

Acquire 语义(Load-Acquire):
  屏障后的所有读写,不能移到 Acquire 操作之前
  → "进入临界区后看到的数据都是最新的"

Release 语义(Store-Release):
  屏障前的所有读写,不能移到 Release 操作之后
  → "离开临界区前写的数据,其他线程进入临界区后都能看到"
Thread A (writer)          Thread B (reader)
  write x = 42               
  [Release]         ←对应→  [Acquire]
  write flag = true           while (!flag) {}
                              read x   → 保证看到 42

Java 内存模型(JMM)

设计目标

JMM 是 Java 语言规范(JLS §17.4)定义的抽象内存模型,目标是:

  1. 屏蔽不同 CPU 架构的差异,提供统一的并发语义
  2. 允许 JVM 做合理优化(不过度限制性能)
  3. 给程序员明确的可见性保证

主内存与工作内存

JMM 定义了一个抽象的内存结构:

jmm memory model

关键规则

这个模型是抽象概念,不是 JVM 的实际实现,实际上对应 CPU 的寄存器、Store Buffer、Cache 等硬件结构。

8 种原子操作详解

JMM 规定线程与内存之间的交互,通过 8 种原子操作来描述(来源:JLS 第二版 §17.1)。每种操作的作用域数据流方向如下:

操作 作用域 数据流向 一句话含义
read(读取) 主内存 主内存 → 松耦合传输 把变量值从主内存取出,与后续 load 松耦合配对(JLS 原文:"transmits master copy to working memory for use by a later load action")
load(载入) 工作内存 松耦合传输 → 工作内存副本 把 read 传来的值装入工作内存,形成变量副本(JLS 原文:"puts value transmitted from main memory by a read action into working copy")
use(使用) 工作内存 工作内存副本 → 执行引擎 把工作内存的副本值传给执行引擎(JLS 原文:"transfers the contents of the thread's working copy of a variable to the thread's execution engine")
assign(赋值) 工作内存 执行引擎 → 工作内存副本 把执行引擎的运算结果写回工作内存副本(JLS 原文:"transfers a value from the thread's execution engine into the thread's working copy of a variable")
store(存储) 工作内存 工作内存副本 → 松耦合传输 把工作内存副本的值取出,与后续 write 松耦合配对(JLS 原文:"transmits the value of a variable from the thread's working memory to main memory, for use by a subsequent write operation")
write(写入) 主内存 松耦合传输 → 主内存 把 store 传来的值写入主内存变量(JLS 原文:"puts a value transmitted by a store action from the thread's working memory into a shared variable in main memory")
lock(锁定) 主内存 将主内存变量标记为某线程独占,其他线程不可操作
unlock(解锁) 主内存 释放主内存变量的独占标记,允许其他线程重新锁定

"松耦合传输"说明:read 与 load、store 与 write 必须成对出现,但两步之间存在时延(JLS 原文:"there may be some transit time between main memory and a working memory"),因此称为松耦合(loosely coupled)。"传输通道"并非 JLS 术语,是对这段时延的通俗化抽象。

两条数据通路:

从主内存读到执行引擎:read ──→ load ──→ use
                     主内存    工作内存     执行引擎

从执行引擎写回主内存:assign ──→ store ──→ write
                     工作内存    工作内存     主内存

常见混淆点:

拆成两步的设计是刻意的:JMM 可以在 read/load 之间、store/write 之间插入其他操作(如屏障),为可见性规则留出空间。

数据竞争与顺序一致性

JMM 不保证顺序一致性,但保证:正确同步的程序(无数据竞争)具有顺序一致性的执行结果。


happens-before 规则

happens-before 是 JMM 的核心承诺,定义了操作之间的可见性保证。

定义

若操作 A happens-before 操作 B,则:

注意:happens-before 不等于 时间上的先后,而是可见性和顺序保证

八条基本规则

规则 内容
程序顺序规则 同一线程内,前面的操作 happens-before 后面的操作
监视器锁规则 unlock 操作 happens-before 后续对同一锁的 lock 操作
volatile 变量规则 对 volatile 变量的写 happens-before 后续对该变量的读
线程启动规则 Thread.start() happens-before 被启动线程的所有操作
线程终止规则 线程的所有操作 happens-before Thread.join() 的返回
线程中断规则 对线程的 interrupt() happens-before 被中断线程检测到中断
对象终结规则 对象构造完成 happens-before finalize() 方法
传递性规则 若 A hb B,B hb C,则 A hb C

传递性的力量

传递性规则让 happens-before 可以"借力":

// 线程 A
x = 42;           // ① 写 x
flag = true;      // ② 写 volatile flag(hb ③)

// 线程 B
if (flag) {       // ③ 读 volatile flag
    print(x);     // ④ 读 x
}

推导链:① hb ②(程序顺序规则)→ ② hb ③(volatile 规则)→ ③ hb ④(程序顺序规则) → 由传递性:① hb ④,线程 B 读到的 x 一定是 42。

规则的重要性层次

8 条规则并非同等重要,可按角色分三层:

层次 规则 角色
地基 程序顺序规则 线程内顺序的基础,缺失则单线程推理崩溃
跨线程入口 volatile 变量规则、监视器锁规则 真正建立跨线程 hb 边;没有它们,线程间没有任何同步语义
放大器 传递性规则 将孤立的 hb 边焊接成推导链;没有它,volatile 只能保证直接相关的变量可见
特定场景的桥 线程启动/终止/中断、对象终结规则 覆盖具体同步场景,可从上两层推导,属于便利规则

传递性是放大器而非核心:上例中如果没有 ② hb ③(volatile 规则建立的跨线程边),传递性无从发挥;如果没有传递性,volatile 只能证明 flag 可见,推断不出 x 可见。三者缺一不可,互为依托。

规则的最小化视角

从能否被其他规则推导来看:

4 条核心规则(不可消除):程序顺序、监视器锁、volatile 变量、传递性

4 条便利规则(可从核心推导):

便利规则 等价于
线程启动规则 Thread.start() 内部有隐式 Release 语义,新线程第一条指令有隐式 Acquire 语义
线程终止规则 线程结束处有隐式 Release,Thread.join() 返回处有隐式 Acquire
线程中断规则 中断标志具有 volatile 语义
对象终结规则 终结队列内部使用了同步机制,是监视器锁规则的应用

JSR-133 正式论文(Manson、Pugh、Adve,POPL 2005)将整个 JMM 精简为 3 个概念:

happens-before = 传递闭包(程序顺序  ∪  synchronizes-with)

synchronizes-with 一个关系统一了所有跨线程同步机制(volatile、monitor、thread start/join 等)。JLS 展开成 8 条,是为了让程序员直接查表,而非每次从头推导。


volatile 深度解析

两个语义

volatile 提供两个保证:

  1. 可见性:对 volatile 变量的写立即对其他线程可见(写刷主内存,读从主内存取)
  2. 禁止重排序:对 volatile 变量的读写不能与前后的读写发生重排序

JMM 规定的屏障插入规则

JMM 要求编译器在 volatile 读写的前后插入特定屏障(这是 JMM 层面的规范,JVM 实现会根据具体 CPU 优化):

volatile 写之前:StoreStore Barrier(前面的写不能越过 volatile 写)
volatile 写之后:StoreLoad  Barrier(volatile 写不能越过后面的读)

volatile 读之后:LoadLoad  Barrier(volatile 读后的读不能越过 volatile 读)
volatile 读之后:LoadStore Barrier(volatile 读后的写不能越过 volatile 读)

图示:

        StoreStore
        ↓
[volatile 写]
        ↓
        StoreLoad(最重的屏障)

[volatile 读]
        ↓
        LoadLoad + LoadStore

x86 上的实现

x86 TSO 已保证 LoadLoad、StoreStore、LoadStore 的顺序,JVM 只需在 volatile 写后插入一条 LOCK ADD 指令(充当 StoreLoad 屏障),volatile 读则不需要额外指令。

; volatile 写:flag = true
MOV [flag], 1
LOCK ADD [rsp], 0   ; StoreLoad 屏障,冲刷 Store Buffer

; volatile 读:r = flag
MOV EAX, [flag]     ; 无额外屏障指令(x86 TSO 已保证)

ARM 上的实现

ARM 需要更多屏障:

; volatile 写:使用 STLR(Store-Release)
STLR W0, [flag]   ; 隐含 Release 语义

; volatile 读:使用 LDAR(Load-Acquire)
LDAR W0, [flag]   ; 隐含 Acquire 语义

volatile 的限制

volatile 不能替代 synchronized 的场景:

// 错误!volatile 不保证复合操作的原子性
volatile int count = 0;

// 线程 A 和 B 同时执行:
count++;   // 这是 read-modify-write,三步操作,volatile 无法保证原子性

count++ 实际是三步:① 读 count → ② count+1 → ③ 写 count 两个线程可能同时读到同一个旧值,各自 +1 后写回,结果只增加了 1。

volatile 适用场景:


synchronized 与内存模型

获取/释放语义

synchronized 在 JMM 层面提供监视器锁规则

进入 synchronized 块(monitorenter)= Acquire 语义
  → 清空工作内存,从主内存重新读取变量

退出 synchronized 块(monitorexit)= Release 语义
  → 将工作内存中的修改刷回主内存

这比 volatile 更强:不仅保证可见性,还保证互斥(同一时刻只有一个线程能执行临界区)。

底层实现

synchronized (lock) {
    // 临界区
}

编译后:

monitorenter  ← 对应 lock 前的 Acquire 屏障(LoadLoad + LoadStore)
  临界区代码
monitorexit   ← 对应 unlock 前的 Release 屏障(StoreStore + StoreLoad)

x86 上,JVM 实现 synchronized 使用 LOCK CMPXCHG(CAS)尝试获取锁,底层隐含了全屏障语义。

synchronized vs volatile

维度 synchronized volatile
可见性
原子性 ✅(整个临界区原子) ❌(仅单次读/写原子)
禁止重排序
阻塞线程 是(竞争时挂起) 否(非阻塞)
性能 较重 轻量
适用场景 复合操作、互斥 状态标志、简单发布

final 与安全发布

初始化安全性

JMM 对 final 字段有特殊规定:只要对象被正确构造(构造函数中不发布 this 引用),final 字段的值对所有线程可见,无需额外同步。

class ImmutablePoint {
    final int x;
    final int y;

    ImmutablePoint(int x, int y) {
        this.x = x;   // final 写
        this.y = y;   // final 写
    }                 // 构造函数结束:插入 StoreStore 屏障
}

// 发布(通过 volatile 或 synchronized)
volatile ImmutablePoint point;
point = new ImmutablePoint(1, 2);  // 其他线程读到 point 后
                                   // 保证看到 x=1, y=2

为什么 final 需要特殊处理

没有 final 保证时,可能发生构造函数写入和对象引用发布的重排序:

正常顺序:  x = 1 → y = 2 → this 引用写到共享变量
重排序后:  this 引用写到共享变量 → x = 1 → y = 2

其他线程可能通过共享变量读到对象引用,此时 x, y 还是默认值 0。final 的屏障保证了这种重排序不会发生。

不安全发布的例子

this 逸出发生在 x = 42 之前——构造函数还没完成,final 字段的"冻结"动作尚未触发:

class Unsafe {
    final int x;  // 即便是 final,也救不了

    Unsafe() {
        GlobalHolder.ref = this;  // ① this 逸出:x 尚未赋值!
        x = 42;                   // ② final 写——对已看到 ① 的线程已经太晚了
    }                             // ③ 构造函数结束:freeze 才在此刻触发
}
// 其他线程在 ① 之后、② 之前读到 ref != null,此时 x 还是默认值 0
// final 的可见性保证依赖 freeze(③),this 提前逸出绕过了它

对比正确写法——this 只能在构造函数返回后通过安全发布机制(volatile / synchronized)传递给其他线程:

// ✅ 安全:构造函数内不泄露 this
volatile Unsafe ref;
ref = new Unsafe();  // 构造完成后再发布,freeze 已触发,x == 42 对所有线程可见

双重检查锁定(DCL)

问题背景

单例模式的懒加载中,为避免每次 getInstance() 都加锁:

// 错误的 DCL(Java 5 之前)
class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {          // 第一次检查(无锁)
            synchronized (Singleton.class) {
                if (instance == null) {  // 第二次检查(有锁)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

为什么错误?

instance = new Singleton() 在字节码层面是三步:

  1. 分配内存空间
  2. 初始化对象(调用构造函数)
  3. 将引用赋给 instance

步骤 2 和 3 可能被重排序:先执行 3(instance 不为 null),再执行 2(对象未初始化)。另一个线程在第一次检查时看到 instance != null,直接使用了一个未初始化的对象。

正确的 DCL:volatile

class Singleton {
    // volatile 禁止步骤 2 和 3 的重排序
    private static volatile Singleton instance;

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

volatile 的 StoreStore 屏障保证:构造函数中的写操作(步骤 2)一定在 instance 赋值(步骤 3)之前完成。

更优雅:静态内部类(推荐)

class Singleton {
    private Singleton() {}

    private static class Holder {
        static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

利用类加载机制保证初始化的线程安全,无需 volatile,无需 synchronized,延迟加载。


各层对照总结

从硬件到 Java 的完整映射

Java 代码
  volatile 写 / synchronized 退出
       ↓ JIT 编译
JVM 字节码指令 + 屏障语义
       ↓ 平台相关实现
x86:  LOCK ADD / MFENCE
ARM:  STLR / LDAR / DMB ISH
       ↓ 硬件执行
冲刷 Store Buffer → Cache 一致性(MESI)→ 其他核可见

各机制保证对比

机制 可见性 原子性 有序性 性能代价
volatile 仅单次读/写 ✅ 禁止重排
synchronized ✅ 整块临界区 中(无竞争时 JIT 优化后低)
final ✅ 初始化安全 ✅ 构造完成前不泄露 极低
CAS 与 ABA 问题|AtomicXxx ✅ CAS 原子操作 部分(取决于 memory order) 低(CAS 自旋)
AQS 原理深度解析|ReentrantLock

常见误区

误区 正确理解
volatile 保证原子性 ❌ 只保证单次读/写原子,count++ 不原子
synchronized 只是互斥锁 ❌ 还有内存可见性语义(Acquire/Release)
happens-before = 时间先后 ❌ 是可见性和顺序的逻辑保证,不是时钟顺序
x86 不需要内存屏障 ❌ 编译器重排序仍需屏障,StoreLoad 仍需 MFENCE
volatile 可以替代锁 ❌ 复合操作(check-then-act)必须用锁

在线工具

MESI 协议动态模拟器

VivioJS MESI 模拟器(Trinity College Dublin,Jeremy Jones 制作)

可视化演示 3 个 CPU + 主内存的缓存一致性协议,支持手动触发 read/write 操作,实时观察缓存行状态(I/E/M/S)和总线事务。

操作方式

  1. 点击图表区域激活动画
  2. 点击各 CPU 上的 read / write 按钮,触发对 a0a3 的内存操作
  3. 观察缓存行颜色(绿色 = 命中,灰色 = stale)和状态标签的变化

推荐入门序列(从 RESET 开始)

  1. CPU0 read a0 → 状态变 E(Exclusive,仅 CPU0 有且与内存一致)
  2. CPU0 write a0 → 状态变 M(Modified,内存已过期)
  3. CPU1 read a0 → CPU0 缓存介入,同步给 CPU1 和内存 → 状态变 S(Shared)
  4. CPU1 write a0 → 使其他缓存中的 a0 失效 → CPU1 状态变 E

参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。

发表评论