编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • JVM内存模型与对象
      • 类加载与双亲委派
      • 垃圾回收与GC调优
      • 异常体系与JVM机制
      • 字节码指令集javap实战
      • JIT编译与去优化机制
      • JVM性能诊断工具链
      • OOM八大现场全景剖析
      • JVM参数调优全景图
      • GraalVM与AOT编译原理
      • HashMap底层哈希设计
      • String不可变与常量池
      • ArrayList与LinkedList源码
      • ConcurrentHashMap并发
      • TreeMap与红黑树原理
      • LinkedHashMap与LRU实现
      • Java数字类型原理
      • Object通用方法的契约
      • 泛型擦除与类型系统
      • 枚举原理与最佳实践
      • 注解原理与编译期处理
      • Lambda与引用底层原理
      • Stream原理与流水线设计
      • Optional设计原理
      • Record密封类与模式
      • 反射机制与动态代理
      • MethodHandle与VarHandle
      • 三大字节码框架对比
      • JavaAgent与Instrumentation机制
      • AOP三种实现路线对比
      • synchronized与锁升级
      • volatile与JMM内存模型
        • 9.1 开篇疑问
        • 9.2 从硬件说起
          • 9.2.1 CPU缓存架构
          • 9.2.2 缓存一致性问题
          • 9.2.3 MESI缓存一致性协议
          • 9.2.4 Store Buffer与指令重排
        • 9.3 Java内存模型JMM
          • 9.3.1 JMM的抽象模型
          • 9.3.2 JMM的八大原子操作
          • 9.3.3 三大核心问题
          • 9.3.4 happens-before规则
          • 9.3.5 as-if-serial与happens-before的关系
        • 9.4 volatile深度剖析
          • 9.4.1 volatile保证可见性
          • 9.4.2 volatile保证有序性
          • 9.4.3 volatile不保证原子性
          • 9.4.4 底层实现之内存屏障
          • 9.4.5 x86架构下的lock前缀指令
          • 9.4.6 volatile的字节码与汇编
        • 9.5 volatile经典应用
          • 9.5.1 双重检查锁单例
          • 9.5.2 状态标志位
          • 9.5.3 安全发布对象
          • 9.5.4 volatile读写模式
        • 9.6 volatile与其他同步机制对比
          • 9.6.1 volatile vs synchronized
          • 9.6.2 volatile vs Atomic
          • 9.6.3 volatile vs Lock
        • 9.7 JMM的设计权衡
          • 9.7.1 为什么不强制所有变量立即可见
          • 9.7.2 不同CPU架构的内存模型差异
          • 9.7.3 final字段的内存语义
        • 9.8 常见面试深度问题
        • 9.9 总结与核心要点
      • 线程池核心源码设计
      • Thread线程生命周期
      • AQS同步框架源码
      • 并发锁三剑客
      • CAS和Atomic深入分析
      • 五大同步器对比
      • CompletableFuture异步
      • IO模型演进BIO到AIO
      • ByteBuffer与堆外内存
      • 序列化原理与替代方案
      • 文件IO与NIO.2
      • 面向对象的真意
      • JDK设计模式上
      • JDK设计模式下
      • SPI与模块化设计
  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Java入门精通
  • 专栏博客
杨充
2026-06-02
目录

volatile与JMM内存模型

# 32.volatile与JMM内存模型

# 目录介绍

  • 9.1 开篇疑问
  • 9.2 从硬件说起
    • 9.2.1 CPU缓存架构
    • 9.2.2 缓存一致性问题
    • 9.2.3 MESI缓存一致性协议
    • 9.2.4 Store Buffer与指令重排
  • 9.3 Java内存模型JMM
    • 9.3.1 JMM的抽象模型
    • 9.3.2 JMM的八大原子操作
    • 9.3.3 三大核心问题
    • 9.3.4 happens-before规则
    • 9.3.5 as-if-serial与happens-before的关系
  • 9.4 volatile深度剖析
    • 9.4.1 volatile保证可见性
    • 9.4.2 volatile保证有序性
    • 9.4.3 volatile不保证原子性
    • 9.4.4 底层实现之内存屏障
    • 9.4.5 x86架构下的lock前缀指令
    • 9.4.6 volatile的字节码与汇编
  • 9.5 volatile经典应用
    • 9.5.1 双重检查锁单例
    • 9.5.2 状态标志位
    • 9.5.3 安全发布对象
    • 9.5.4 volatile读写模式
  • 9.6 volatile与其他同步机制对比
    • 9.6.1 volatile vs synchronized
    • 9.6.2 volatile vs Atomic
    • 9.6.3 volatile vs Lock
  • 9.7 JMM的设计权衡
    • 9.7.1 为什么不强制所有变量立即可见
    • 9.7.2 不同CPU架构的内存模型差异
    • 9.7.3 final字段的内存语义
  • 9.8 常见面试深度问题
  • 9.9 总结与核心要点

# 9.1 开篇疑问

疑惑:volatile 只是一个关键字,它凭什么能保证可见性?为什么不能保证原子性?双重检查锁单例为什么必须用 volatile?什么是"指令重排序"?JMM 和 JVM 内存区域是什么关系?

答疑:要理解 volatile,必须先理解 Java 内存模型(JMM)。JMM 是 Java 并发编程的理论基础,它定义了线程如何与内存交互、多线程之间如何通信。volatile 是 JMM 提供的最轻量级的同步机制。

# 9.2 从硬件说起

# 9.2.1 CPU缓存架构

现代 CPU 的速度远远快于内存。为了填补速度鸿沟,CPU 引入了多级缓存:

CPU Core 1          CPU Core 2
┌──────────┐       ┌──────────┐
│ 寄存器    │       │ 寄存器    │
│ L1 Cache │       │ L1 Cache │   ← 每核私有,速度最快(~1ns)
│ L2 Cache │       │ L2 Cache │   ← 每核私有(~3ns)
└────┬─────┘       └────┬─────┘
     └──────┬───────────┘
        L3 Cache                   ← 多核共享(~10ns)
            │
       主内存 (RAM)                 ← 所有CPU共享(~100ns)
1
2
3
4
5
6
7
8
9
10

各级缓存的延迟对比:

层级 大小 延迟 倍数
寄存器 ~1KB ~0.3ns 1x
L1 Cache ~64KB ~1ns 3x
L2 Cache ~256KB ~3ns 10x
L3 Cache ~8MB ~10ns 33x
主内存 ~16GB ~100ns 333x
磁盘 ~1TB ~10ms 33,000,000x

# 9.2.2 缓存一致性问题

每个 CPU 核心有自己的缓存。当两个核心同时缓存了同一个变量,一个核心修改了值,另一个核心怎么知道?

Core1 缓存: x = 0      Core2 缓存: x = 0
Core1 执行: x = 1       (Core2 不知道x已经变了)
Core1 缓存: x = 1      Core2 缓存: x = 0  ← 不一致!
1
2
3

# 9.2.3 MESI缓存一致性协议

硬件层面通过 MESI 协议解决缓存一致性:

MESI 四种状态:
M (Modified)  : 已修改,本核独占,与主内存不一致
E (Exclusive) : 独占,本核独占,与主内存一致
S (Shared)    : 共享,多核都有缓存,与主内存一致
I (Invalid)   : 无效,缓存行已失效,需要重新从主内存或其他核读取

状态转换示例:
1. Core1 读 x → 缓存行状态: E(独占)
2. Core2 也读 x → 两个核的缓存行状态都变为: S(共享)
3. Core1 修改 x = 1 → Core1 状态: M, Core2 状态: I(失效)
4. Core2 再读 x → Core1 将修改写回,两核状态: S
1
2
3
4
5
6
7
8
9
10
11

MESI 的局限:核心间通过总线嗅探(Bus Snooping)通知状态变化,但这会阻塞 CPU。为了避免阻塞,CPU 引入了 Store Buffer 和 Invalidate Queue。

# 9.2.4 Store Buffer与指令重排

CPU 优化架构:
          Store Buffer      Invalidate Queue
CPU Core ──→ [写缓冲区] ──→ Cache ──→ 主内存
              (异步写入)

问题:
1. Store Buffer 让写操作异步化 → 其他核心可能读到旧值
2. Invalidate Queue 让失效通知延迟处理 → 本核心可能读到过期缓存

这就是为什么需要内存屏障(Memory Barrier)来强制刷新/排空这些缓冲区
1
2
3
4
5
6
7
8
9
10

CPU 层面的指令重排序:

编译器重排序  → 指令级并行重排序 → 内存系统重排序

示例:
// 原始代码
a = 1;     // S1
flag = true; // S2

// CPU 可能重排为:
flag = true; // S2 先执行(flag 在缓存中,写入更快)
a = 1;       // S1 后执行(a 需要从主内存加载)

单线程下结果一样,但多线程下:
线程B看到 flag=true 时,a 可能还是 0
1
2
3
4
5
6
7
8
9
10
11
12
13

# 9.3 Java内存模型JMM

# 9.3.1 JMM的抽象模型

JMM(Java Memory Model)是对硬件内存模型的抽象,定义了线程和主内存之间的关系:

线程1 工作内存          线程2 工作内存
┌──────────────┐     ┌──────────────┐
│ 变量x的副本    │     │ 变量x的副本    │
│ 变量y的副本    │     │ 变量y的副本    │
└──────┬───────┘     └──────┬───────┘
       │   save/load         │   save/load
       └───────┬─────────────┘
          主内存 (Main Memory)
       ┌──────────────────┐
       │ 共享变量 x, y      │
       └──────────────────┘
1
2
3
4
5
6
7
8
9
10
11
  • 主内存:所有线程共享的内存区域,存储共享变量
  • 工作内存:每个线程私有,存储共享变量的副本

注意区分:JMM 是抽象的内存模型规范,不等于 JVM 内存区域(堆、栈、方法区)。JMM 的主内存大致对应堆中的对象实例部分,工作内存大致对应 CPU 缓存和寄存器。

# 9.3.2 JMM的八大原子操作

JMM 定义了 8 个操作来描述变量从主内存到工作内存的交互:

主内存 → 工作内存:
  lock    → 锁定主内存变量,标记为线程独占
  read    → 从主内存读取变量值到传输通道
  load    → 将 read 的值放入工作内存的变量副本

工作内存内:
  use     → 将工作内存的变量值传递给执行引擎
  assign  → 将执行引擎的结果赋值给工作内存的变量

工作内存 → 主内存:
  store   → 将工作内存的变量值传输到主内存的传输通道
  write   → 将 store 的值写入主内存的变量

主内存:
  unlock  → 释放主内存变量的锁定状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

八大操作的规则约束:

  1. read 和 load、store 和 write 必须成对出现
  2. assign 后必须 store+write 回主内存(不允许丢弃工作内存的修改)
  3. 未 assign 不允许 store+write(不允许无故写回主内存)
  4. use 前必须 load(保证从主内存获取最新值)
  5. lock 后清空工作内存副本,需要重新 load
  6. unlock 前必须 store+write 回主内存

# 9.3.3 三大核心问题

1. 可见性问题:一个线程修改了共享变量,另一个线程看不到。

// 经典案例:线程B可能永远看不到 flag = true
boolean flag = false;

// 线程A
flag = true;

// 线程B
while (!flag) {
    // 可能死循环!B的工作内存中flag一直是false
    // 因为 JIT 可能将 flag 的值提升到寄存器
    // 或者 CPU 缓存中的旧值一直不刷新
}
1
2
3
4
5
6
7
8
9
10
11
12

2. 原子性问题:一个操作在执行过程中被其他线程打断。

// i++ 不是原子操作,字节码分解为:
// 1. getstatic  → 读取 i 的值
// 2. iconst_1   → 准备常量 1
// 3. iadd       → 计算 i + 1
// 4. putstatic  → 写回 i 的值
// 在步骤 1 和 4 之间,其他线程可能已经修改了 i
1
2
3
4
5
6

3. 有序性问题:编译器和 CPU 为了优化可能重排指令顺序。

int a = 0;
boolean flag = false;

// 线程A
a = 1;           // 语句1
flag = true;     // 语句2

// 编译器可能将语句2排在语句1前面执行(单线程下结果一样)
// 但多线程下,线程B可能观察到 flag=true 但 a 还未赋值

// 线程B
if (flag) {
    int b = a;   // 可能 b=0!
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 9.3.4 happens-before规则

JMM 通过 happens-before 规则来保证多线程下的可见性和有序性:

规则 含义 示例
程序顺序规则 同一线程内,前面的操作 h-b 后面的操作 a=1 h-b b=a
volatile规则 volatile 写 h-b 后续的 volatile 读 写flag h-b 读flag
锁规则 unlock h-b 后续的 lock 释放锁 h-b 获取锁
传递性 A h-b B,B h-b C → A h-b C 链式传递
线程启动规则 Thread.start() h-b 线程内的所有操作 start h-b run内操作
线程终止规则 线程内所有操作 h-b Thread.join() 返回 run内操作 h-b join返回
线程中断规则 interrupt() h-b 被中断线程检测到中断 interrupt h-b isInterrupted
对象终结规则 构造函数 h-b finalize() init h-b finalize

注意:happens-before 不是"时间上先发生",而是"先发生的操作对后发生的操作可见"。

// happens-before 的传递性应用
int a = 0;
volatile boolean flag = false;

// 线程A
a = 1;           // 操作1
flag = true;     // 操作2(volatile 写)

// 线程B
if (flag) {      // 操作3(volatile 读)
    int b = a;   // 操作4
}

// 推导:
// 操作1 h-b 操作2(程序顺序规则)
// 操作2 h-b 操作3(volatile规则)
// 操作1 h-b 操作3(传递性)
// 操作3 h-b 操作4(程序顺序规则)
// 所以 操作1 h-b 操作4,即 a=1 对操作4可见,b=1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 9.3.5 as-if-serial与happens-before的关系

as-if-serial 语义:
  → 不管怎么重排序,单线程程序的执行结果不能被改变
  → 编译器和处理器必须遵守
  → 给单线程程序员的保证

happens-before 规则:
  → 不管怎么重排序,正确同步的多线程程序的执行结果不能被改变
  → JMM 向程序员提供的可见性保证
  → 给多线程程序员的保证

两者的关系:
  as-if-serial 是 happens-before 在单线程中的体现
  happens-before 是 as-if-serial 在多线程中的推广
1
2
3
4
5
6
7
8
9
10
11
12
13

# 9.4 volatile深度剖析

# 9.4.1 volatile保证可见性

volatile 变量的写操作会立即刷新到主内存,读操作会从主内存重新加载,跳过工作内存缓存。

volatile boolean flag = false;

// 线程A
flag = true;  // 写入后立即刷新到主内存

// 线程B
while (!flag) {
    // 每次读取都从主内存获取最新值,不会死循环
}
1
2
3
4
5
6
7
8
9

可见性的底层保证:

volatile 写:
1. 修改工作内存中的变量值
2. 将修改后的值 store+write 到主内存
3. 通知其他 CPU 核心该缓存行失效

volatile 读:
1. 使工作内存中该变量的副本无效
2. 从主内存 read+load 最新值到工作内存
3. 使用最新值
1
2
3
4
5
6
7
8
9

# 9.4.2 volatile保证有序性

volatile 通过禁止指令重排序来保证有序性。具体规则:

  • volatile 写之前的操作不会被重排到写之后
  • volatile 读之后的操作不会被重排到读之前
  • volatile 写 happens-before 后续的 volatile 读
int a = 0;
volatile boolean flag = false;

// 线程A
a = 1;           // 普通写
flag = true;     // volatile 写 → 保证 a=1 在 flag=true 之前执行

// 线程B
if (flag) {      // volatile 读
    int b = a;   // 保证读到 a=1(不会被重排到flag读之前)
}
1
2
3
4
5
6
7
8
9
10
11

volatile 重排序规则表:

              第二个操作
              普通读/写   volatile读   volatile写
第一个操作
普通读/写       可以         可以         不可以
volatile读     不可以       不可以       不可以
volatile写     可以         不可以       不可以

关键规则:
1. volatile 写之后不能跟任何操作的重排序
2. volatile 读之前不能跟任何操作的重排序
3. volatile 写之前的普通写不能重排到 volatile 写之后
4. volatile 读之后的普通读不能重排到 volatile 读之前
1
2
3
4
5
6
7
8
9
10
11
12

# 9.4.3 volatile不保证原子性

volatile int count = 0;

// 10个线程各执行1000次 count++
// 最终 count 不一定是10000

// 原因:count++ 分三步
// 1. 读取 count (volatile 保证读到最新值)
// 2. count + 1 (计算)
// 3. 写入 count (volatile 保证立即刷新)
// 但步骤1和3之间,其他线程可能已经修改了count

// 具体场景:
// 线程A 读取 count=5
// 线程B 读取 count=5
// 线程A 计算 5+1=6,写入 count=6
// 线程B 计算 5+1=6,写入 count=6
// 两个线程各加了一次,但 count 只增加了1

// 解决方案1:AtomicInteger
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();  // CAS 保证原子性

// 解决方案2:synchronized
synchronized (this) {
    count++;
}

// 解决方案3:LongAdder(高并发场景推荐)
LongAdder adder = new LongAdder();
adder.increment();
long total = adder.sum();
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
27
28
29
30
31

# 9.4.4 底层实现之内存屏障

volatile 在字节码层面通过内存屏障(Memory Barrier)指令实现:

volatile 写:
  StoreStore Barrier    // 禁止上面的普通写与下面的volatile写重排
  ───────────────────
  [volatile 写操作]
  ───────────────────
  StoreLoad Barrier     // 禁止volatile写与下面的volatile读/写重排

volatile 读:
  [volatile 读操作]
  ───────────────────
  LoadLoad Barrier      // 禁止volatile读与下面的普通读重排
  ───────────────────
  LoadStore Barrier     // 禁止volatile读与下面的普通写重排
1
2
3
4
5
6
7
8
9
10
11
12
13

四种内存屏障的含义:

屏障类型 指令示例 效果
LoadLoad Load1; LoadLoad; Load2 Load1 数据加载先于 Load2
StoreStore Store1; StoreStore; Store2 Store1 刷新到主内存先于 Store2
LoadStore Load1; LoadStore; Store2 Load1 数据加载先于 Store2 刷新
StoreLoad Store1; StoreLoad; Load2 Store1 刷新到主内存先于 Load2

StoreLoad 是开销最大的屏障,它是一个全能屏障(同时具有其他三种屏障的效果)。

# 9.4.5 x86架构下的lock前缀指令

在 x86 架构上,volatile 写会生成一条 lock addl $0x0, (%rsp) 指令:

0x01a3de24: lock addl $0x0,(%rsp)

lock 前缀指令的两个效果:
1. 将当前 CPU 的缓存行写回主内存
   → 相当于 Store 操作立即生效
2. 使其他 CPU 核心的缓存行失效(通过总线锁或缓存锁)
   → 其他核心下次读取时必须从主内存获取

在 x86 上:
→ StoreStore 和 LoadLoad 屏障是空操作(x86 的强内存模型已保证)
→ StoreLoad 屏障通过 lock 前缀指令实现
→ 所以 volatile 的开销主要在写操作(需要 lock 指令)
1
2
3
4
5
6
7
8
9
10
11
12

# 9.4.6 volatile的字节码与汇编

public class VolatileTest {
    volatile int x = 0;
    
    public void write() {
        x = 1;
    }
    
    public int read() {
        return x;
    }
}
1
2
3
4
5
6
7
8
9
10
11

字节码层面:volatile 字段在 .class 文件中标记了 ACC_VOLATILE 访问标志。

// javap -v VolatileTest.class
volatile int x;
  descriptor: I
  flags: ACC_VOLATILE
1
2
3
4

JIT 编译后的汇编(使用 -XX:+PrintAssembly):

# volatile 写
mov    $0x1, 0xc(%rsi)          # 将 1 写入 x 的内存地址
lock addl $0x0, (%rsp)          # 内存屏障!

# volatile 读
mov    0xc(%rsi), %eax          # 直接从内存读取
# x86 的强内存模型保证了 volatile 读不需要额外屏障
1
2
3
4
5
6
7

# 9.5 volatile经典应用

# 9.5.1 双重检查锁单例

public class Singleton {
    // 必须加 volatile!
    private static volatile Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {                // 第一次检查(无锁)
            synchronized (Singleton.class) {
                if (instance == null) {        // 第二次检查(有锁)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

疑惑:为什么必须加 volatile?

论证:instance = new Singleton() 在 JVM 中分三步:

1. memory = allocate()     // 分配内存空间
2. ctorInstance(memory)    // 初始化对象
3. instance = memory       // 将 instance 指向分配的内存

指令重排序可能导致顺序变为 1→3→2:
1. memory = allocate()     // 分配内存空间
3. instance = memory       // instance 不为 null了!但对象未初始化!
   ← 此时线程B在第一次检查时发现 instance != null
   ← 直接返回一个未初始化的对象!
2. ctorInstance(memory)    // 初始化(但线程B已经拿走了未初始化的对象)
1
2
3
4
5
6
7
8
9
10

volatile 禁止了 2 和 3 的重排序,保证对象完全初始化后才对其他线程可见。

不用 volatile 的替代方案(基于类加载机制):

public class Singleton {
    private Singleton() {}
    
    // 利用 JVM 保证 <clinit>() 线程安全的特性
    private static class Holder {
        static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 9.5.2 状态标志位

// volatile 最适合的场景:一个线程写,多个线程读
volatile boolean shutdownRequested = false;

// 工作线程
public void run() {
    while (!shutdownRequested) {
        doWork();
    }
}

// 管理线程
public void shutdown() {
    shutdownRequested = true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 9.5.3 安全发布对象

// 不安全的发布
public class UnsafePublish {
    private int value;
    
    public UnsafePublish(int value) {
        this.value = value;
    }
    
    // 其他线程可能看到 obj != null 但 value = 0 的中间状态
    public static UnsafePublish obj;
    
    public static void init() {
        obj = new UnsafePublish(42);
        // 可能重排为: 先 obj=地址, 再 value=42
    }
}

// 安全的发布方式1:volatile
public static volatile UnsafePublish obj;

// 安全的发布方式2:synchronized
public static synchronized void init() {
    obj = new UnsafePublish(42);
}

// 安全的发布方式3:final 字段
public class SafePublish {
    private final int value;  // final 字段在构造函数结束后保证对所有线程可见
    
    public SafePublish(int value) {
        this.value = value;
    }
}
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
27
28
29
30
31
32
33

# 9.5.4 volatile读写模式

// volatile 的经典使用模式:开销最小化
public class CheesyCounter {
    private volatile int value;
    
    // 读操作:直接读取 volatile 变量(无锁)
    public int getValue() {
        return value;
    }
    
    // 写操作:使用 synchronized 保证原子性
    public synchronized int increment() {
        return value++;
    }
    
    // 好处:读操作极其频繁时,避免了加锁开销
    // 写操作通过 synchronized 保证原子性
    // volatile 保证写入后其他线程立即可见
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 9.6 volatile与其他同步机制对比

# 9.6.1 volatile vs synchronized

特性 volatile synchronized
原子性 不保证 保证
可见性 保证 保证
有序性 保证(禁止重排序) 保证(临界区内可重排,但结果对外原子)
阻塞 不阻塞 可能阻塞(锁竞争)
锁升级 无 偏向锁→轻量锁→重量锁
适用场景 状态标志、DCL 复合操作、临界区
性能开销 极低(仅写操作有屏障) 较高(锁的获取和释放)
// volatile 够用的场景:
// 1. 写操作不依赖当前值(flag = true)
// 2. 变量不与其他变量共同参与不变式约束

// 必须用 synchronized 的场景:
// 1. check-then-act(检查后操作)
// 2. read-modify-write(读-改-写,如 i++)
// 3. 多个变量需要原子性更新
1
2
3
4
5
6
7
8

# 9.6.2 volatile vs Atomic

// volatile 不能保证 i++ 的原子性
volatile int count = 0;
// count++ 仍然有并发问题

// AtomicInteger 通过 CAS 保证原子性
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();  // 原子操作

// AtomicInteger 内部就使用了 volatile
public class AtomicInteger extends Number {
    private volatile int value;  // volatile 保证可见性
    
    public final int incrementAndGet() {
        // CAS 循环,保证原子性
        return U.getAndAddInt(this, VALUE, 1) + 1;
    }
}

// CAS 的底层:
// Unsafe.compareAndSwapInt(obj, offset, expect, update)
// → 对应 CPU 的 CMPXCHG 指令(带 lock 前缀)
// → 一条指令完成比较+交换,硬件级原子操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 9.6.3 volatile vs Lock

// ReentrantLock 在 lock/unlock 时也有内存语义
// lock() → volatile 读
// unlock() → volatile 写

// ReentrantLock 的 state 字段就是 volatile
public class ReentrantLock {
    abstract static class Sync extends AbstractQueuedSynchronizer {
        // AQS 的 state 是 volatile int
    }
}

// 所以:
// lock.lock()   → 读取 volatile state → 后续操作不会被重排到 lock 之前
// lock.unlock() → 写入 volatile state → 之前的操作不会被重排到 unlock 之后
// 效果等同于 synchronized 的内存语义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 9.7 JMM的设计权衡

# 9.7.1 为什么不强制所有变量立即可见

如果所有变量都是 volatile 的(立即可见):
→ 每次写操作都要刷新到主内存
→ 每次读操作都要从主内存加载
→ CPU 缓存完全失效
→ 性能退回到没有缓存的时代
→ 性能下降 100 倍以上

JMM 的设计哲学:
→ 默认不保证可见性(允许 CPU 缓存优化)
→ 程序员通过 volatile/synchronized/Lock 按需保证
→ 只在需要的地方付出同步开销
→ 平衡了性能和安全性
1
2
3
4
5
6
7
8
9
10
11
12

# 9.7.2 不同CPU架构的内存模型差异

强内存模型 ←────────────────→ 弱内存模型
   x86/x64        ARM/Power        Alpha

x86(TSO, Total Store Ordering):
→ Store-Store 不会重排序(天然保证)
→ Load-Load 不会重排序(天然保证)
→ Load-Store 不会重排序(天然保证)
→ 只有 Store-Load 可能重排序
→ 所以 volatile 在 x86 上只需要在写操作后加 StoreLoad 屏障

ARM(弱内存模型):
→ 所有四种重排序都可能发生
→ volatile 在 ARM 上需要更多的屏障指令
→ 开销更大

JMM 的价值:
→ 屏蔽了不同硬件的差异
→ Java 代码一次编写,在任何架构上的并发语义一致
→ "Write once, run anywhere" 在并发场景的体现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 9.7.3 final字段的内存语义

// final 字段也有特殊的内存语义
public class FinalFieldExample {
    final int x;
    int y;
    static FinalFieldExample obj;
    
    public FinalFieldExample() {
        x = 3;  // final 字段写入
        y = 4;  // 普通字段写入
    }
    
    public static void writer() {
        obj = new FinalFieldExample();
    }
    
    public static void reader() {
        FinalFieldExample local = obj;
        if (local != null) {
            int a = local.x;  // 保证读到 3(final 语义保证)
            int b = local.y;  // 可能读到 0(无保证)
        }
    }
}

// final 字段的内存语义:
// 1. 构造函数中的 final 字段写入 h-b 构造函数结束
// 2. 其他线程读取该对象的 final 字段时,保证能看到构造函数中设置的值
// 3. 前提:不能在构造函数中将 this 引用逸出(否则语义失效)
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
27
28

# 9.8 常见面试深度问题

Q1:volatile 能否替代 synchronized?

不能完全替代。volatile 只能保证可见性和有序性,不能保证原子性。只有在以下条件同时满足时,才能用 volatile 替代 synchronized:

  1. 写操作不依赖变量的当前值(或只有一个线程写)
  2. 变量不参与涉及多个变量的不变式约束

Q2:volatile 变量的读写性能如何?

volatile 读:在 x86 上几乎零开销(不需要额外屏障)
volatile 写:需要 lock 前缀指令,开销约为普通写的 10-20 倍
             但远小于 synchronized(无需进入/退出 Monitor)
1
2
3

Q3:JMM 的工作内存是不是线程栈?

不完全是。JMM 的工作内存是抽象概念,对应的物理实现包括:CPU 寄存器、L1/L2 Cache、Store Buffer 等。线程栈中的局部变量不需要同步(因为是线程私有的),JMM 关心的是共享变量。

Q4:volatile 数组能保证数组元素的可见性吗?

volatile int[] arr = new int[10];
// volatile 只保证 arr 引用本身的可见性
// 不保证 arr[0], arr[1] 等元素的可见性!

arr[0] = 1;  // 不具有 volatile 语义
arr = arr;   // 重新赋值引用才有 volatile 语义(但这很蠢)

// 解决方案:使用 AtomicIntegerArray
AtomicIntegerArray atomicArr = new AtomicIntegerArray(10);
atomicArr.set(0, 1);  // 原子操作 + volatile 语义
1
2
3
4
5
6
7
8
9
10

Q5:long 和 double 的非原子性问题

// JMM 允许虚拟机将 long/double 的读写分成两次 32 位操作
// 这意味着在 32 位 JVM 上,long/double 的读写不是原子的

long value = 0L;
// 线程A: value = 0x1234567890ABCDEFL
// 线程B: 可能读到 0x12345678_00000000(高32位新值 + 低32位旧值)

// 解决方案:加 volatile
volatile long value = 0L;  // volatile 的 long/double 保证原子读写

// 注意:几乎所有 64 位 JVM 都已经保证了 long/double 的原子读写
// 这个问题在实践中很少遇到,但面试可能会问
1
2
3
4
5
6
7
8
9
10
11
12

# 9.9 总结与核心要点

JMM 设计哲学:

  1. 抽象硬件差异:屏蔽不同 CPU 架构的缓存一致性差异,提供统一的并发模型
  2. 平衡性能与安全:不强制所有变量立即可见(太慢),通过 volatile/synchronized 按需保证
  3. happens-before:用简洁的规则替代复杂的底层实现细节
  4. 最小化约束:只禁止会影响程序正确性的重排序,允许无害的优化

核心对比:

特性 volatile synchronized Atomic Lock
可见性 保证 保证 保证 保证
原子性 不保证 保证 保证 保证
有序性 保证 保证 不完全 保证
阻塞 不阻塞 阻塞 不阻塞 可中断
适用场景 标志位/DCL 临界区 计数器 复杂同步

volatile 的本质:通过内存屏障禁止重排序 + 强制刷新/失效缓存,以极低的开销实现可见性和有序性保证。

核心要点速查:

问题 答案
volatile 保证什么 可见性 + 有序性,不保证原子性
volatile 的底层实现 内存屏障(x86 上是 lock addl)
DCL 为何需要 volatile 防止对象创建过程中的指令重排序
happens-before 是什么 JMM 定义的可见性保证规则
JMM 工作内存对应什么 CPU 缓存 + 寄存器 + Store Buffer
volatile 数组元素是否可见 不保证,需要 AtomicIntegerArray
x86 上 volatile 读的开销 几乎零(强内存模型天然保证)
上次更新: 2026/06/10, 11:13:41
synchronized与锁升级
线程池核心源码设计

← synchronized与锁升级 线程池核心源码设计→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式