为什么 volatile 能保证可见性?为什么 synchronized 既保证原子性又保证可见性?答案藏在 CPU 缓存一致性协议和内存屏障中。本文将从硬件层面的 MESI 协议出发,逐步上升到 Java 内存模型(JMM),揭示并发可见性问题的完整因果链。


Part 1: CPU 缓存架构

为什么需要 CPU 缓存

现代 CPU 的运算速度远超内存访问速度,两者之间存在巨大的速度鸿沟

操作 延迟 相对速度
CPU 寄存器访问 ~0.3 ns 1x
L1 Cache 访问 ~1 ns 3x
L2 Cache 访问 ~4 ns 13x
L3 Cache 访问 ~12 ns 40x
主内存访问 ~100 ns 333x

如果 CPU 每次都直接访问主内存,大部分时间都在等待数据。缓存利用了时间局部性空间局部性,将最近和附近的数据保存在更快的存储中。

多级缓存架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌──────────────────────────────────────────────────────┐
CPU 0
│ ┌──────┐ ┌──────┐ │
│ │ Core0│ │ Core1│ │
│ │ ┌──┐ │ │ ┌──┐ │ │
│ │ │L1│ │ │ │L1│ │ ← 每个核心私有 │
│ │ └──┘ │ │ └──┘ │ │
│ │ ┌──┐ │ │ ┌──┐ │ │
│ │ │L2│ │ │ │L2│ │ ← 每个核心私有(部分架构共享) │
│ │ └──┘ │ │ └──┘ │ │
│ └──────┘ └──────┘ │
│ ┌─────────────────┐ │
│ │ L3 │ ← 同一 CPU 内所有核心共享 │
│ └─────────────────┘ │
└──────────┬───────────────────────────────────────────┘

┌──────▼──────┐
│ 主内存 │ ← 所有 CPU 共享
└─────────────┘

缓存行(Cache Line)

CPU 缓存的最小操作单位是缓存行(Cache Line),通常为 64 字节

当 CPU 读取一个变量时,会将该变量所在的整个缓存行(64 字节)加载到缓存中。这意味着:

  • 相邻的变量会被一起加载(空间局部性)
  • 修改一个变量可能影响同一缓存行中的其他变量(伪共享问题

MESI 协议的必要性

当多个 CPU 核心同时访问同一块内存时,如果没有缓存一致性协议,会出现数据不一致的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

### MESI 的四种状态

**MESI** 是最经典的缓存一致性协议,每个缓存行有四种状态:

| 状态 | 全称 | 含义 | 可以直接读? | 可以直接写? |
|------|------|------|------------|------------|
| **M(Modified)** | 已修改 | 只有本核心有,且已修改,与主内存不一致 |||
| **E(Exclusive)** | 独占 | 只有本核心有,与主内存一致 || ✅(变为 M) |
| **S(Shared)** | 共享 | 多个核心都有,与主内存一致 || ❌(需先失效其他核心) |
| **I(Invalid)** | 无效 | 缓存行无效,不可使用 |||

### 状态转换

```mermaid
stateDiagram-v2
[*] --> I
I --> E: 本核心读取(其他核心无此数据)
I --> S: 本核心读取(其他核心有此数据)
E --> M: 本核心写入
E --> S: 其他核心读取
E --> I: 其他核心写入
S --> M: 本核心写入(其他核心的副本变为 I)
S --> I: 其他核心写入
M --> S: 其他核心读取(先写回主内存)
M --> I: 其他核心写入(先写回主内存)

详细的状态转换场景

场景一:Core 0 独占读取

1
2
3
4
5
6
7
初始状态:x 不在任何缓存中

Core 0 读取 x
1. Core 0 发出 Read 请求
2. 总线嗅探:没有其他核心有 x
3. 从主内存加载 x 到 Core 0 的缓存
4. 状态:E(独占)

场景二:Core 1 也读取 x

1
2
3
4
5
6
7
8
Core 0 缓存: x = 42 (E)

Core 1 读取 x:
1. Core 1 发出 Read 请求
2. 总线嗅探:Core 0 有 x(状态 E)
3. Core 0 将 x 提供给 Core 1(可能通过缓存到缓存传输)
4. Core 0 状态:E → S
5. Core 1 状态:I → S

场景三:Core 0 写入 x

1
2
3
4
5
6
7
8
9
10
Core 0 缓存: x = 42 (S)
Core 1 缓存: x = 42 (S)

Core 0 写入 x = 100
1. Core 0 发出 Invalidate 请求
2. Core 1 收到 Invalidate,将 x 标记为 I(无效)
3. Core 1 发送 Invalidate Acknowledge
4. Core 0 收到所有 Acknowledge 后,执行写入
5. Core 0 状态:S → M
6. Core 1 状态:S → I

场景四:Core 1 读取被修改的 x

1
2
3
4
5
6
7
8
9
10
11
Core 0 缓存: x = 100 (M)
Core 1 缓存: x = 42 (I)

Core 1 读取 x:
1. Core 1 发出 Read 请求
2. 总线嗅探:Core 0 有 x(状态 M)
3. Core 0 将 x = 100 写回主内存
4. Core 0 将 x = 100 提供给 Core 1
5. Core 0 状态:M → S
6. Core 1 状态:I → S
7. Core 1 读到的值:100(最新值)

总线嗅探(Bus Snooping)

MESI 协议通过总线嗅探实现:每个核心的缓存控制器都在监听总线上的所有事务。当检测到与自己缓存行相关的操作时,自动更新状态。

总线嗅探的局限性

  • 总线带宽有限,核心数增多时成为瓶颈
  • 现代多核处理器(如 AMD EPYC)使用**目录协议(Directory Protocol)**替代总线嗅探

Part 3: Store Buffer 与 Invalidate Queue

为什么 MESI 还不够

MESI 协议保证了缓存一致性,但它有一个性能问题:写入操作需要等待所有其他核心的 Invalidate Acknowledge

1
2
3
4
5
6
7
8
9
10
11
12
13
Core 0 写入 x(状态 S):
1. 发送 Invalidate 到 Core 1, Core 2, Core 3
2. 等待 Core 1 的 Acknowledge ← 可能需要几十个时钟周期
3. 等待 Core 2 的 Acknowledge ← 可能需要几十个时钟周期
4. 等待 Core 3 的 Acknowledge ← 可能需要几十个时钟周期
5. 才能执行写入

这个等待时间对 CPU 来说是不可接受的。

### Store Buffer(写缓冲区)

为了避免等待,CPU 引入了 **Store Buffer**

Core 0 写入 x = 100:

  1. 将 x = 100 写入 Store Buffer(立即返回,不等待)
  2. 异步发送 Invalidate 到其他核心
  3. 收到所有 Acknowledge 后,将 Store Buffer 中的值写入缓存
1
2
3
4
5
6
7
8
9
10
11

**Store Buffer 带来的问题**:

```java
// 初始值:a = 0, b = 0

// Core 0 // Core 1
a = 1; b = 1;
x = b; y = a;

// 可能的结果:x = 0, y = 0(都读到了旧值!)

原因:Core 0 写入 a = 1 时,值还在 Store Buffer 中,尚未对 Core 1 可见。Core 1 同理。

Invalidate Queue(失效队列)

类似地,接收 Invalidate 请求的核心也不想立即处理(处理需要时间),于是引入了 Invalidate Queue

1
2
3
4
Core 1 收到 Invalidate(x):
1. 将 Invalidate 请求放入 Invalidate Queue
2. 立即发送 Acknowledge(不等待实际失效)
3. 稍后处理 Queue 中的请求,将缓存行标记为 I

Invalidate Queue 带来的问题:Core 1 可能在处理 Invalidate 之前读取了缓存中的旧值。

重排序的根源

Store Buffer 和 Invalidate Queue 是 CPU 指令重排序的硬件根源:

重排序类型 原因 示例
Store-Store 重排序 Store Buffer 中的写入可能乱序刷出 先写 a 后写 b,但 b 先对其他核心可见
Load-Load 重排序 Invalidate Queue 中的失效可能延迟处理 读到了已经被其他核心修改的旧值
Store-Load 重排序 Store Buffer 中的写入对本核心的后续读取不可见 写入 a 后读取 b,但 a 的写入还在 Store Buffer 中
Load-Store 重排序 较少见,某些架构可能发生

Part 4: 内存屏障(Memory Barrier)

什么是内存屏障

内存屏障(Memory Barrier / Memory Fence) 是 CPU 提供的指令,用于限制重排序:

屏障类型 作用 实现方式
Store Barrier(写屏障) 屏障前的写入必须在屏障后的写入之前完成 刷出 Store Buffer
Load Barrier(读屏障) 屏障前的读取必须在屏障后的读取之前完成 处理 Invalidate Queue
Full Barrier(全屏障) 同时具有读屏障和写屏障的效果 刷出 Store Buffer + 处理 Invalidate Queue

x86 的内存模型

x86 架构使用 TSO(Total Store Order) 内存模型,这是一个相对强的内存模型:

重排序类型 x86 是否允许
Load-Load ❌ 不允许
Load-Store ❌ 不允许
Store-Store ❌ 不允许
Store-Load ✅ 允许

x86 只允许 Store-Load 重排序(写入后的读取可能读到旧值)。这意味着在 x86 上,大多数情况下不需要显式的内存屏障,只有 Store-Load 场景需要 mfence 指令。

TSO 的实现机制

  • 所有写操作进入 Store Buffer
  • 写操作按顺序从 Store Buffer 刷出到缓存
  • 读操作会检查 Store Buffer,如果命中则返回 Store Buffer 中的值
  • 这保证了 Store-Store 顺序,但无法保证 Store-Load 顺序

x86 内存屏障指令

  • mfence:全屏障(禁止所有重排序)
  • sfence:Store 屏障(禁止 Store-Store 和 Store-Load 重排序)
  • lfence:Load 屏障(禁止 Load-Load 和 Load-Store 重排序)
  • lock 前缀:隐式全屏障(如 lock addl $0, (%rsp)

ARM/RISC-V 的弱内存模型

ARM 和 RISC-V 使用弱内存模型,允许所有四种重排序:

重排序类型 ARM 是否允许 RISC-V 是否允许
Load-Load ✅ 允许 ✅ 允许
Load-Store ✅ 允许 ✅ 允许
Store-Store ✅ 允许 ✅ 允许
Store-Load ✅ 允许 ✅ 允许

因此在这些架构上,需要更多的内存屏障指令。

ARM 内存屏障指令(ARMv8):

  • DMB (Data Memory Barrier):数据内存屏障
  • DSB (Data Synchronization Barrier):数据同步屏障
  • ISB (Instruction Synchronization Barrier):指令同步屏障

RISC-V 内存屏障指令

  • fence rw, rw:全屏障
  • fence r, r:读屏障
  • fence w, w:写屏障

跨平台差异的影响

  • 某些并发 Bug 在 x86 上不会出现,但在 ARM 上会出现
  • JVM 需要为不同架构生成不同的屏障指令
  • ARM/PowerPC 等弱内存模型架构需要更多的屏障指令,可能影响性能

Part 5: Java 内存模型(JMM)

JMM 的设计目标

Java 内存模型(Java Memory Model, JMM)定义在 JSR-133(Java 5)中,其目标是:

  1. 为程序员提供足够强的保证:正确同步的程序在所有平台上行为一致
  2. 为编译器和 CPU 提供足够的优化空间:不过度限制重排序

JMM 的抽象模型

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──────────┐  ┌──────────┐  ┌──────────┐
Thread 1 │ │ Thread 2 │ │ Thread 3
│ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │
│ │工作内存│ │ │ │工作内存│ │ │ │工作内存│ │
│ │(本地) │ │ │ │(本地) │ │ │ │(本地) │ │
│ └──┬───┘ │ │ └──┬───┘ │ │ └──┬───┘ │
└────┼─────┘ └────┼─────┘ └────┼─────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────┐
│ 主内存(Main Memory) │
│ 所有共享变量存储在这里 │
└─────────────────────────────────────┘

注意:JMM 是一个抽象模型,不是物理实现。"工作内存"对应的是 CPU 缓存、Store Buffer、寄存器等硬件结构的抽象。

Happens-Before 关系

Happens-Before 是 JMM 的核心概念:如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见。

JMM 定义了以下 Happens-Before 规则:

规则 说明 示例
程序顺序规则 同一线程中,前面的操作 happens-before 后面的操作 a = 1; b = 2;a=1 hb b=2
Monitor 锁规则 unlock 操作 happens-before 后续对同一锁的 lock 操作 synchronized 块解锁后的操作对后续加锁可见
volatile 规则 volatile 写 happens-before 后续对同一 volatile 变量的读 volatile flag = true; 后的读取保证看到 true
线程启动规则 Thread.start() happens-before 该线程中的任何操作 主线程调用 start() 前的操作对新线程可见
线程终止规则 线程中的任何操作 happens-before Thread.join() 返回 join() 返回后,线程的所有修改对调用者可见
中断规则 Thread.interrupt() happens-before 被中断线程检测到中断 调用 interrupt() 后,线程能检测到中断状态
终结器规则 构造函数完成 happens-before finalize() 开始 对象构造完成 before 垃圾回收调用 finalize()
传递性 如果 A hb B,B hb C,则 A hb C A=1 hb B=2 hb C=3 ⇒ A=1 hb C=3

Happens-Before 不等于时间上的先后

Happens-Before 是一种可见性保证,不是时间顺序。即使操作 A 在时间上先于操作 B 执行,如果没有 Happens-Before 关系,B 也不一定能看到 A 的结果。


Part 6: volatile 的实现原理

volatile 的语义

  1. 可见性:对 volatile 变量的写入,对后续读取该变量的线程立即可见
  2. 有序性:禁止 volatile 读写与前后操作的重排序
  3. 不保证原子性volatile int count; count++ 不是原子操作

volatile 的内存屏障

JVM 在 volatile 读写前后插入内存屏障:

volatile 写

1
2
3
4
普通写操作
StoreStore Barrier ← 禁止上面的普通写与下面的 volatile 写重排序
volatile 写操作
StoreLoad Barrier ← 禁止 volatile 写与下面的 volatile 读/写重排序

volatile 读

1
2
3
4
volatile 读操作
LoadLoad Barrier ← 禁止 volatile 读与下面的普通读重排序
LoadStore Barrier ← 禁止 volatile 读与下面的普通写重排序
普通读操作

volatile 在 x86 上的实现

由于 x86 的 TSO 内存模型已经禁止了大部分重排序,volatile 在 x86 上的实现非常轻量:

  • volatile 读:普通的 mov 指令(不需要额外屏障)
  • volatile 写mov + lock addl $0, (%rsp)lock 前缀指令充当 StoreLoad Barrier)

lock 前缀指令的效果:

  1. 锁定缓存行(或总线)
  2. 将 Store Buffer 中的所有写入刷出到缓存
  3. 使其他核心的对应缓存行失效

JIT 编译产物分析

对于以下代码:

1
2
volatile int counter;
counter++;

在 x86 上,JIT 编译器(如 HotSpot C2)会生成类似以下的汇编代码:

1
2
3
4
5
6
7
8
9
10
; volatile 读
mov eax, [counter] ; 从内存读取 counter 值到 eax

; 递增操作(非原子)
inc eax ; eax++

; volatile 写
lock add dword [counter], 1 ; 原子递增(如果使用 Atomic 类)
; 或者:
mov [counter], eax ; 写入 eax 到 counter(非原子,需要额外同步)

注意volatile int counter; counter++ 不是原子操作,实际包含:

  1. volatile 读 counter
  2. 递增
  3. volatile 写 counter

这三个步骤之间可能被其他线程插入,导致竞态条件。如果需要原子递增,应使用 AtomicInteger.incrementAndGet()

AtomicInteger 的实现

1
2
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();

JIT 会生成:

1
2
3
4
5
6
7
; CAS (Compare-And-Swap) 循环
retry:
mov eax, [counter] ; 读取当前值
mov ebx, eax
inc ebx ; 计算新值
lock cmpxchg [counter], ebx ; CAS 操作
jne retry ; 如果失败则重试

lock cmpxchg 指令是原子的,保证了递增操作的原子性。

volatile 的典型应用

双重检查锁定(DCL)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
private static volatile Singleton instance; // 必须是 volatile

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

为什么 instance 必须是 volatile?

instance = new Singleton() 实际上包含三步:

  1. 分配内存
  2. 调用构造函数初始化
  3. 将引用赋值给 instance

如果没有 volatile,步骤 2 和 3 可能被重排序:先赋值引用(此时对象尚未初始化),另一个线程看到 instance != null,直接使用了未初始化的对象。

volatile 禁止了这种重排序,保证构造函数完成后才赋值引用。

状态标志

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TaskRunner {
private volatile boolean running = true;

public void run() {
while (running) { // volatile 读,保证看到最新值
doWork();
}
}

public void stop() {
running = false; // volatile 写,立即对其他线程可见
}
}

Part 7: synchronized 的内存语义

synchronized 的三重保证

保证 说明
原子性 同一时刻只有一个线程执行临界区代码
可见性 解锁时将工作内存刷新到主内存,加锁时从主内存重新加载
有序性 临界区内的操作不会与临界区外的操作重排序

synchronized 的内存屏障

1
2
3
4
5
6
7
8
9
10
11
加锁(monitorenter):
1. 获取锁
2. 清空工作内存(从主内存重新加载所有共享变量)
→ 相当于 LoadLoad + LoadStore Barrier

临界区代码执行

解锁(monitorexit):
1. 将工作内存中的修改刷新到主内存
→ 相当于 StoreStore + StoreLoad Barrier
2. 释放锁

synchronized 的锁升级

Java 6 引入了锁升级机制,根据竞争程度自动选择最优的锁实现:

1
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
锁状态 适用场景 实现方式 性能
偏向锁 只有一个线程访问 在对象头记录线程 ID 最快(无 CAS)
轻量级锁 少量线程交替访问(无竞争) CAS + 自旋 快(用户态)
重量级锁 多线程竞争 OS Mutex 慢(内核态切换)

对象头中的锁标志位

1
2
3
4
5
6
7
8
9
10
11
┌──────────────────────────────────────────────────────┐
│ 对象头(Mark Word) │
├──────────────────────────────────────┬───────────────┤
│ 内容 │ 锁标志位(2bit) │
├──────────────────────────────────────┼───────────────┤
│ hashCode | age | 0 01 │ 无锁
│ thread ID | epoch | age | 1 01 │ 偏向锁
│ 指向栈中锁记录的指针 │ 00 │ 轻量级锁
│ 指向 Monitor 的指针 │ 10 │ 重量级锁
│ 空 │ 11 │ GC 标记
└──────────────────────────────────────┴───────────────┘

Part 8: 伪共享(False Sharing)

问题描述

当两个线程分别修改同一缓存行中的不同变量时,虽然它们操作的是不同的数据,但由于 MESI 协议以缓存行为单位工作,会导致缓存行在两个核心之间不断失效和重新加载——这就是伪共享(False Sharing)

1
2
3
4
5
6
7
8
缓存行(64 字节):
┌─────────┬─────────┬──────────────────────────┐
x (8B) │ y (8B) │ padding (48B) │
└─────────┴─────────┴──────────────────────────┘

Core 0 修改 x → 整个缓存行在 Core 1 中失效
Core 1 修改 y → 整个缓存行在 Core 0 中失效
→ 缓存行在两个核心之间"乒乓",性能急剧下降

解决方案:缓存行填充

Java 8 之前:手动填充

1
2
3
4
5
6
7
8
9
public class PaddedAtomicLong {
// 前填充(7 个 long = 56 字节)
private long p1, p2, p3, p4, p5, p6, p7;

private volatile long value;

// 后填充(7 个 long = 56 字节)
private long p8, p9, p10, p11, p12, p13, p14;
}

Java 8+:@Contended 注解

1
2
3
4
@sun.misc.Contended  // JVM 自动添加填充
public class PaddedValue {
public volatile long value;
}

需要 JVM 参数 -XX:-RestrictContended 才能在非 JDK 内部类中使用。

实际案例

Disruptor(高性能队列)使用缓存行填充来避免伪共享:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Disruptor 的 Sequence 类
class LhsPadding {
protected long p1, p2, p3, p4, p5, p6, p7;
}

class Value extends LhsPadding {
protected volatile long value;
}

class RhsPadding extends Value {
protected long p9, p10, p11, p12, p13, p14, p15;
}

public class Sequence extends RhsPadding {
// value 字段被前后各 56 字节的填充包围
// 保证 value 独占一个缓存行
}

Java 的 Thread 类中的 @Contended 使用:

1
2
3
4
5
6
7
8
9
// java.lang.Thread 中的 ThreadLocalRandom 字段
@jdk.internal.vm.annotation.Contended("tlr")
long threadLocalRandomSeed;

@jdk.internal.vm.annotation.Contended("tlr")
int threadLocalRandomProbe;

@jdk.internal.vm.annotation.Contended("tlr")
int threadLocalRandomSecondarySeed;

Part 9: final 的内存语义

final 字段的特殊保证

JMM 对 final 字段提供了特殊的可见性保证:

在构造函数中对 final 字段的写入,与随后将该对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

这意味着:只要对象的引用不在构造函数中逸出,其他线程看到该对象时,final 字段一定已经被正确初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class FinalExample {
private final int x;
private int y;

public FinalExample() {
x = 42; // final 字段写入
y = 42; // 普通字段写入
}
}

// 另一个线程
FinalExample obj = sharedRef; // 读取共享引用
if (obj != null) {
int a = obj.x; // 保证读到 42
int b = obj.y; // 不保证读到 42(可能读到 0)
}

构造函数中的 this 逸出

如果在构造函数中将 this 引用泄露给其他线程,final 的保证将失效:

1
2
3
4
5
6
7
8
public class UnsafePublication {
private final int value;

public UnsafePublication(EventSource source) {
source.registerListener(this); // this 逸出!构造函数尚未完成
value = 42;
}
}

其他线程可能通过 registerListener 获取到未完全构造的对象,此时 value 可能还是 0。


Part 10: 从硬件到 JMM 的完整映射

完整的因果链

1
2
3
4
5
6
7
8
硬件层面                          JMM 层面
───────── ────────
CPU 缓存(L1/L2/L3) → 工作内存
主内存(RAM) → 主内存
Store Buffer → 写入不立即可见
Invalidate Queue → 读取可能读到旧值
MESI 协议 → 缓存一致性(但不够)
内存屏障指令 → volatile / synchronized 的底层实现

volatile vs synchronized vs final

特性 volatile synchronized final
可见性 ✅(构造函数完成后)
原子性 ❌(仅单次读/写) N/A
有序性 ✅(禁止重排序) ✅(临界区内外不重排) ✅(构造函数内)
性能 低开销 中-高开销 零开销
适用场景 状态标志、DCL 临界区保护 不可变对象

实际编程建议

  1. 优先使用不可变对象final 字段 + 不可变类,天然线程安全
  2. 其次使用 volatile:适合简单的状态标志和发布不可变对象
  3. 需要复合操作时使用 synchronized 或 Lock:保证原子性
  4. 考虑使用 java.util.concurrentAtomicIntegerConcurrentHashMap 等已经处理好了并发问题
  5. 注意伪共享:高性能场景下使用 @Contended 或手动填充

总结

层次 概念 作用
硬件 CPU 缓存 + MESI 协议 保证缓存一致性
硬件 Store Buffer + Invalidate Queue 提升性能,但引入可见性问题
硬件 内存屏障指令 限制重排序,恢复可见性
JVM volatile / synchronized / final 将内存屏障封装为语言级语义
JMM Happens-Before 规则 为程序员提供可见性保证

核心认知:并发可见性问题的根源不是"线程切换",而是 CPU 缓存和写缓冲区导致的数据不一致。volatile 和 synchronized 通过插入内存屏障,强制将数据从 Store Buffer 刷出到缓存/主内存,并处理 Invalidate Queue 中的失效请求,从而保证可见性。

参考资料