volatile关键字与内存屏障
约 2014 字大约 7 分钟
javavolatilememory
2025-03-05
概述
volatile 是Java中最轻量的同步机制。它提供两个核心语义:可见性(一个线程对 volatile 变量的修改对其他线程立即可见)和 有序性(禁止指令重排序)。但它 不保证原子性,不能替代锁。
可见性问题
在现代多核CPU架构中,每个CPU核心有自己的缓存(L1/L2),线程对共享变量的修改可能停留在本地缓存中,其他线程无法及时看到。
// 可见性问题示例
public class VisibilityProblem {
private boolean running = true; // 缺少volatile
public void stop() {
running = false; // 线程A修改
}
public void run() {
while (running) { // 线程B可能永远看不到修改
// JIT可能将running提升到循环外(hoisting)
}
}
}
// 修复:添加volatile
private volatile boolean running = true;volatile的可见性保证
当一个变量被声明为 volatile 时:
- 写操作:将工作内存中的值刷新到主内存
- 读操作:从主内存重新加载最新值(使本地缓存无效)
有序性与指令重排
编译器和处理器为了优化性能,可能对指令进行重排序。重排序有三种类型:
- 编译器重排序:编译器在不改变单线程语义下重排语句顺序
- 处理器重排序:CPU乱序执行指令
- 内存系统重排序:缓存/写缓冲区导致的可见顺序不同
// 重排序示例
int a = 0; // 语句1
boolean flag = false; // 语句2
// Thread 1
a = 1; // 语句3
flag = true; // 语句4
// 语句3和4可能被重排为先执行4再执行3
// Thread 2
if (flag) { // 语句5
int b = a; // 语句6:可能读到 a = 0(因为重排序)
}使用 volatile 修饰 flag 后,语句3一定在语句4之前执行(禁止重排序),且语句3对变量 a 的修改对语句6可见。
内存屏障(Memory Barrier)
volatile 的语义通过插入内存屏障指令实现。JMM定义了四种屏障:
| 屏障类型 | 指令示例 | 作用 |
|---|---|---|
| LoadLoad | Load1; LoadLoad; Load2 | 确保Load1的数据在Load2及后续加载前完成 |
| StoreStore | Store1; StoreStore; Store2 | 确保Store1的写入对其他处理器可见后再执行Store2 |
| LoadStore | Load1; LoadStore; Store2 | 确保Load1的数据在Store2及后续写入前完成 |
| StoreLoad | Store1; StoreLoad; Load2 | 确保Store1的写入对所有处理器可见后再执行Load2(最重的屏障) |
volatile读写的屏障插入策略
- volatile写之前插入 StoreStore 屏障 → 确保前面的普通写不被重排到volatile写之后
- volatile写之后插入 StoreLoad 屏障 → 确保volatile写对后续volatile读可见
- volatile读之后插入 LoadLoad 和 LoadStore 屏障 → 确保后续的读写不被重排到volatile读之前
DCL(Double-Check Locking)模式
DCL 是 volatile 最经典的应用场景 — 线程安全的延迟初始化单例。
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(); // 关键:需要volatile防止重排序
}
}
}
return instance;
}
}为什么需要volatile?
instance = new Singleton() 这行代码在JVM层面实际包含三步操作:
1. 分配内存空间 memory = allocate()
2. 初始化对象 ctorInstance(memory)
3. 将引用指向分配的内存空间 instance = memory没有volatile时,步骤2和3可能被重排序:
加上volatile后,禁止步骤2和3的重排序,保证对象完全初始化后才对其他线程可见。
happens-before规则
JMM中volatile相关的happens-before规则:
对一个volatile变量的写操作 happens-before 于后续对同一个volatile变量的读操作。
结合传递性规则,volatile写之前的所有操作对volatile读之后的操作都可见:
// 初始值
int a = 0;
volatile boolean flag = false;
// Thread 1
a = 42; // 操作A
flag = true; // 操作B (volatile写)
// A happens-before B(程序顺序规则)
// Thread 2
if (flag) { // 操作C (volatile读)
int r = a; // 操作D → r一定等于42
}
// B happens-before C(volatile规则)
// C happens-before D(程序顺序规则)
// 传递性:A happens-before D → a=42对D可见volatile不保证原子性
public class VolatileNotAtomic {
private volatile int count = 0;
// 错误:volatile不能保证i++的原子性
public void increment() {
count++; // 实际是三步:读 → 加 → 写
}
// 多线程调用increment,最终count值小于预期
// 正确方案1:AtomicInteger
private AtomicInteger atomicCount = new AtomicInteger(0);
public void atomicIncrement() {
atomicCount.incrementAndGet(); // CAS保证原子性
}
// 正确方案2:synchronized
public synchronized void syncIncrement() {
count++;
}
}volatile适用场景
1. 状态标志(最常见)
// 一写多读的标志变量
private volatile boolean shutdown = false;
public void shutdown() { shutdown = true; }
public void doWork() {
while (!shutdown) {
// ...
}
}2. 一次性安全发布
// 对象的安全发布
private volatile Config config;
public void init() {
Config c = new Config();
c.setXxx(...);
config = c; // volatile写保证config引用和对象内容一起可见
}3. 低开销读写锁策略
// 读操作远多于写操作时:volatile读 + synchronized写
private volatile int value;
public int getValue() { return value; } // 无锁读
public synchronized void setValue(int v) { value = v; } // 同步写volatile与final的内存语义对比
| 特性 | volatile | final |
|---|---|---|
| 可见性 | 每次读都从主内存加载 | 构造函数完成后对所有线程可见 |
| 有序性 | 禁止重排序 | 构造函数内的赋值不能重排到构造函数外 |
| 使用场景 | 变量值会被多线程修改 | 不可变字段 |
| 性能开销 | 每次读写有屏障开销 | 仅构造时有屏障 |
常见FAQ
Q: volatile数组/对象的元素是否有volatile语义?
A: volatile修饰引用变量时,只保证引用本身的可见性和有序性,不保证引用指向的对象内部状态。volatile int[] 只保证数组引用可见,数组元素的修改不具备volatile语义。需要用 AtomicIntegerArray 等。
Q: volatile变量能被JIT优化掉吗?
A: 不能。JIT编译器遇到volatile变量的读写时,不会进行常量折叠、死代码消除、循环外提等优化。这也是volatile有一定性能开销的原因。
Q: x86架构下volatile读有性能开销吗?
A: x86是强一致性内存模型(TSO),volatile读几乎没有额外开销(等价于普通读),volatile写会插入StoreLoad屏障(lock前缀指令),有一定开销。ARM/POWER等弱内存模型架构下,读写都可能插入屏障。
总结
volatile 是一个简洁但容易误用的关键字。它适用于"一写多读"的简单同步场景,通过内存屏障保证可见性和有序性。但对于复合操作(如自增、check-then-act),必须使用锁或原子类。理解volatile的内存屏障机制和happens-before语义,是掌握Java并发编程的基础。
贡献者
更新日志
9f6c2-feat: organize wiki content and refresh site setup于