主要作用
作用:
1.保证线程的可见性
2.禁止指令重排
volatile 关键字如何保证线程的可见性和禁止指令重排序,其底层实现主要依赖 内存屏障(Memory Barriers) 和 缓存一致性协议(如 MESI),具体机制如下:
1. 保证线程的可见性
强制读写主内存:volatile 变量的读写直接操作主内存(而不是线程的工作内存),确保修改对其他线程立即可见。
内存屏障:插入 Load 和 Store 屏障,保证:
写操作后:强制将工作内存的修改刷新到主内存(Store 屏障)。
读操作前:强制从主内存重新加载最新值(Load 屏障)。
缓存一致性协议:通过 CPU 的缓存一致性协议(如 MESI),监听总线上的数据修改,使其他线程的缓存行失效,强制重新从主内存加载最新值。
2. 禁止指令重排序 原理:
内存屏障插入规则:
写屏障(Store Barrier):在 volatile 写操作后插入,确保该写操作前的所有操作不会被重排到写之后。
读屏障(Load Barrier):在 volatile 读操作前插入,确保该读操作后的所有操作不会被重排到读之前。
具体屏障类型:
StoreStore 屏障:禁止普通写与 volatile 写重排序。
StoreLoad 屏障:禁止 volatile 写与后续的 volatile 读/写重排序。
LoadLoad + LoadStore 屏障:禁止 volatile 读与普通读/写重排序。
示例:双重检查锁单例模式
java
public class Singleton {
private static volatile Singleton instance;
csharp
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 无指令重排风险
}
}
}
return instance;
}
}
无 volatile 的问题:对象初始化可能被重排序为"分配内存 → 返回引用 → 初始化"(其他线程可能拿到未初始化的对象)。
volatile 的作用:通过内存屏障禁止指令重排,保证"分配内存 → 初始化 → 返回引用"的顺序。
3. 底层实现总结
**特性 实现机制
****可见性 内存屏障强制刷新主内存 + 缓存失效监听(MESI)
****有序性 插入内存屏障禁止编译器和 CPU 重排序
****原子性 不保证(如 volatile int i; i++ 是非原子操作)
**volatile 通过硬件和 JVM 的协作,以性能损耗为代价,实现轻量级的线程安全控制。
可见性【代码演示】
以下是用 Java 代码演示 线程可见性问题 的示例。当没有 volatile
关键字时,线程间对共享变量的修改可能不可见:
java
public class VisibilityDemo {
// 共享变量(无 volatile 修饰)
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
// 线程 A:等待 flag 变为 true
Thread threadA = new Thread(() -> {
while (!flag) {
// 空循环(模拟等待)
}
System.out.println("Thread A: Flag is now true!");
});
// 线程 B:修改 flag 为 true
Thread threadB = new Thread(() -> {
flag = true;
System.out.println("Thread B: Set flag to true.");
});
threadA.start();
// 确保线程 A 先启动
Thread.sleep(100);
threadB.start();
// 等待线程结束
threadA.join();
threadB.join();
}
}
没有 volatile
时:
- 可能现象 :线程 A 的循环永远不会退出(即使线程 B 已将
flag
设置为true
)。 - 原因 :线程 A 在自己的工作内存中缓存了
flag
的初始值false
,无法感知到线程 B 的修改。
添加 volatile
后:
arduino
private static volatile boolean flag = false; // 添加 volatile
- 现象 :线程 A 会立即退出循环,打印
Thread A: Flag is now true!
。 - 原因 :
volatile
强制线程每次访问flag
时都从主内存读取最新值。
可见性问题本质
- JMM(Java 内存模型) :每个线程有自己的工作内存,默认情况下不保证共享变量的修改对其他线程立即可见。
volatile
的强制刷新:通过内存屏障强制线程从主内存读写共享变量。
扩展测试:观察延迟问题
即使没有 volatile
,某些情况下程序可能"偶然"正常退出(例如循环体中有其他操作触发了内存刷新)。但这是不可靠的,实际代码中必须显式保证可见性:
arduino
// 不可靠的写法(可能偶尔正常)
while (!flag) {
// 添加以下代码可能意外触发内存刷新(但不要依赖这种写法!)
// System.out.println("Waiting...");
// Thread.sleep(1);
}
总结
场景 | 结果 | 原因 |
---|---|---|
无 volatile |
可能无限循环 | 线程间不可见性 |
有 volatile |
立即退出循环 | 强制主内存读写 |
通过这个示例可以直观理解 volatile
对可见性的保障作用。
指令重排
- 为什么会指令重排?
单线程下,指令1和指令2的执行顺序可以互换,因为它们的操作是独立的。重排后不会影响最终结果,但能提高 CPU 流水线的执行效率(例如减少指令等待时间)。
1. 编译器优化重排
编译器在生成字节码时,会调整指令顺序以提高性能:
ini
// 原始代码
int x = 1;
int y = 2;
// 编译器可能重排后的字节码顺序
int y = 2;
int x = 1; // 顺序互换,但结果不变
2. CPU 指令级并行重排
现代 CPU 采用流水线、多发射等技术,可能并行执行没有依赖关系的指令:
ini
int a = 10; // 指令A
int b = 20; // 指令B
int result = a * b; // 指令C
CPU 可能同时执行指令A和指令B,再执行指令C。
3. 内存系统重排
CPU 缓存与主内存的交互顺序可能与程序顺序不一致(例如写缓冲区的刷新顺序)。