什么是 volatile?
volatile 是并发编程中一个关键的修饰符,它保证了对共享变量操作的两大特性:可见性(Visibility) 和 有序性(Ordering)。
与 synchronized 锁机制不同,volatile 是一种轻量级的同步机制,它不会引起线程上下文切换和线程调度。但要深入理解它的工作原理,就必须了解底层是如何通过内存屏障(Memory Barrier / Memory Fence) 来实现的。
CPU与指令重排的挑战
为了最大化性能,现代编译器和CPU通常会对指令进行重新排序(Instruction Reordering),前提是这种重排不会改变单线程的执行结果(即遵循“as-if-serial”语义)。
然而,在多线程环境中,指令重排可能导致意料之外的错误,例如一个线程读取到了另一个线程尚未完成写入的中间状态数据。
volatile 与内存屏障的对应关系(以 Java 为例)
Java内存模型(JMM)规定,当对一个 volatile 变量进行读写操作时,必须在汇编级别插入特定的内存屏障,以强制遵守特定的执行顺序和数据同步。
JMM主要定义了四种内存屏障:LoadLoad, StoreStore, LoadStore, StoreLoad。
1. volatile 写操作(Store)
当执行 volatile 变量的写入操作时,JMM会做两件事情:
- 数据刷入主内存: 强制将当前CPU缓存中的新值立即写入主内存。
- 插入屏障: 在 volatile 写操作之后插入一个 StoreLoad 屏障。
屏障效果:
- StoreStore 屏障 (在写操作之前): 确保 volatile 写操作之前的普通写操作不会被重排到 volatile 写操作之后。
- StoreLoad 屏障 (在写操作之后): 这是一个全能屏障,它确保所有的写操作(包括 volatile 自身的写)都对其他处理器可见,并且禁止任何后续的读操作或写操作被重排到 volatile 写操作之前。
// volatile 写操作后的内存屏障效果
private int a; // 普通变量
private volatile boolean flag = false;
// 线程A 执行:
void write() {
a = 10; // 1. 普通写操作
// --> StoreStore 屏障确保 a=10 先于 flag=true 执行
flag = true; // 2. volatile 写操作(写入主内存,并插入 StoreLoad 屏障)
// --> StoreLoad 屏障:保证 a=10 和 flag=true 对所有线程可见
// 之后的操作不会被重排到 flag=true 之前。
}
2. volatile 读操作(Load)
当执行 volatile 变量的读取操作时,JMM也会插入屏障:
- 缓存失效: 强制使当前CPU工作内存中的缓存值失效,必须从主内存中重新加载最新值。
- 插入屏障: 在 volatile 读操作之前和之后插入屏障。
屏障效果:
- LoadLoad 屏障 (在读操作之后): 确保 volatile 读操作之后的所有普通读操作不会被重排到 volatile 读操作之前。
- LoadStore 屏障 (在读操作之后): 确保 volatile 读操作之后的所有普通写操作不会被重排到 volatile 读操作之前。
// volatile 读操作后的内存屏障效果
private int b; // 普通变量
private volatile boolean flag = false;
// 线程B 执行:
void read() {
// --> LoadLoad/LoadStore 屏障确保后续操作不会被重排到 flag 读操作之前
if (flag) { // 1. volatile 读操作(强制从主内存读取)
// 2. 普通读操作
b = a;
}
}
总结:如何实现可见性与禁止重排
1. 保证可见性(Visibility)
可见性是通过 volatile 写操作后的 StoreLoad 屏障 实现的。当一个线程写入 volatile 变量时,StoreLoad 屏障触发一个特殊操作:它将本地缓存数据写回主内存,并向其他CPU发送信号(通常是MESI协议中的Invalidate消息),强制其他线程的本地缓存行失效。这样,其他线程在下次读取该变量时,就必须从主内存重新加载,从而保证了数据的“新鲜”状态。
2. 禁止指令重排(Ordering)
通过在读写操作周围部署的 LoadLoad, LoadStore, StoreStore, StoreLoad 屏障,确保了以下四种重排是被禁止的:
| 操作顺序 | 是否允许重排? | 屏障类型 | 目的 |
|---|---|---|---|
| 1. 普通读/写 -> volatile 读 | 禁止 | LoadLoad/LoadStore | 确保先写操作的结果对 volatile 读可见 |
| 2. volatile 读 -> 普通读/写 | 禁止 | LoadLoad/LoadStore | 确保 volatile 读之后的操作不会被提前 |
| 3. 普通写 -> volatile 写 | 禁止 | StoreStore | 确保 volatile 写之前的写操作先于 volatile 写完成 |
| 4. volatile 写 -> 普通读/写 | 禁止 | StoreLoad | 确保 volatile 写及其同步效果先于后续操作完成 |
正是这些内存屏障,在底层硬件和操作系统层面提供了严格的顺序保证,使得 volatile 成为多线程编程中实现状态标志或简单数据共享的可靠工具。
汤不热吧