volatile与JMM内存模型
# 32.volatile与JMM内存模型
# 目录介绍
- 9.1 开篇疑问
- 9.2 从硬件说起
- 9.3 Java内存模型JMM
- 9.4 volatile深度剖析
- 9.5 volatile经典应用
- 9.6 volatile与其他同步机制对比
- 9.7 JMM的设计权衡
- 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)
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 ← 不一致!
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
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)来强制刷新/排空这些缓冲区
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
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 │
└──────────────────┘
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 → 释放主内存变量的锁定状态
2
3
4
5
6
7
8
9
10
11
12
13
14
15
八大操作的规则约束:
- read 和 load、store 和 write 必须成对出现
- assign 后必须 store+write 回主内存(不允许丢弃工作内存的修改)
- 未 assign 不允许 store+write(不允许无故写回主内存)
- use 前必须 load(保证从主内存获取最新值)
- lock 后清空工作内存副本,需要重新 load
- 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 缓存中的旧值一直不刷新
}
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
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!
}
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
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 在多线程中的推广
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) {
// 每次读取都从主内存获取最新值,不会死循环
}
2
3
4
5
6
7
8
9
可见性的底层保证:
volatile 写:
1. 修改工作内存中的变量值
2. 将修改后的值 store+write 到主内存
3. 通知其他 CPU 核心该缓存行失效
volatile 读:
1. 使工作内存中该变量的副本无效
2. 从主内存 read+load 最新值到工作内存
3. 使用最新值
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读之前)
}
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 读之前
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();
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读与下面的普通写重排
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 指令)
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;
}
}
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
2
3
4
JIT 编译后的汇编(使用 -XX:+PrintAssembly):
# volatile 写
mov $0x1, 0xc(%rsi) # 将 1 写入 x 的内存地址
lock addl $0x0, (%rsp) # 内存屏障!
# volatile 读
mov 0xc(%rsi), %eax # 直接从内存读取
# x86 的强内存模型保证了 volatile 读不需要额外屏障
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;
}
}
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已经拿走了未初始化的对象)
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;
}
}
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;
}
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;
}
}
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 保证写入后其他线程立即可见
}
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. 多个变量需要原子性更新
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 前缀)
// → 一条指令完成比较+交换,硬件级原子操作
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 的内存语义
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 按需保证
→ 只在需要的地方付出同步开销
→ 平衡了性能和安全性
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" 在并发场景的体现
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 引用逸出(否则语义失效)
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:
- 写操作不依赖变量的当前值(或只有一个线程写)
- 变量不参与涉及多个变量的不变式约束
Q2:volatile 变量的读写性能如何?
volatile 读:在 x86 上几乎零开销(不需要额外屏障)
volatile 写:需要 lock 前缀指令,开销约为普通写的 10-20 倍
但远小于 synchronized(无需进入/退出 Monitor)
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 语义
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 的原子读写
// 这个问题在实践中很少遇到,但面试可能会问
2
3
4
5
6
7
8
9
10
11
12
# 9.9 总结与核心要点
JMM 设计哲学:
- 抽象硬件差异:屏蔽不同 CPU 架构的缓存一致性差异,提供统一的并发模型
- 平衡性能与安全:不强制所有变量立即可见(太慢),通过 volatile/synchronized 按需保证
- happens-before:用简洁的规则替代复杂的底层实现细节
- 最小化约束:只禁止会影响程序正确性的重排序,允许无害的优化
核心对比:
| 特性 | volatile | synchronized | Atomic | Lock |
|---|---|---|---|---|
| 可见性 | 保证 | 保证 | 保证 | 保证 |
| 原子性 | 不保证 | 保证 | 保证 | 保证 |
| 有序性 | 保证 | 保证 | 不完全 | 保证 |
| 阻塞 | 不阻塞 | 阻塞 | 不阻塞 | 可中断 |
| 适用场景 | 标志位/DCL | 临界区 | 计数器 | 复杂同步 |
volatile 的本质:通过内存屏障禁止重排序 + 强制刷新/失效缓存,以极低的开销实现可见性和有序性保证。
核心要点速查:
| 问题 | 答案 |
|---|---|
| volatile 保证什么 | 可见性 + 有序性,不保证原子性 |
| volatile 的底层实现 | 内存屏障(x86 上是 lock addl) |
| DCL 为何需要 volatile | 防止对象创建过程中的指令重排序 |
| happens-before 是什么 | JMM 定义的可见性保证规则 |
| JMM 工作内存对应什么 | CPU 缓存 + 寄存器 + Store Buffer |
| volatile 数组元素是否可见 | 不保证,需要 AtomicIntegerArray |
| x86 上 volatile 读的开销 | 几乎零(强内存模型天然保证) |