volatile 关键字如何保证代码的有序性执行?请举例说明。
参考回答
volatile
关键字通过 内存屏障(Memory Barrier) 和 指令重排序规则,保证了代码在多线程环境下的有序性执行。
- 内存屏障:
- 编译器和 CPU 会在
volatile
修饰的变量的读写操作前后插入内存屏障,禁止特定的指令重排序。 - 写操作内存屏障:在写入
volatile
变量后,强制将变量刷新到主内存,同时禁止之前的写操作重排序到volatile
写操作之后。 - 读操作内存屏障:在读取
volatile
变量时,强制线程从主内存中获取最新值,同时禁止之后的读操作重排序到volatile
读操作之前。
- 编译器和 CPU 会在
- 指令重排序规则:
- JMM(Java 内存模型)规定:
volatile
变量的操作与普通变量的操作之间不能发生重排序。 - 通过内存屏障约束读写操作的顺序,确保代码执行的有序性。
- JMM(Java 内存模型)规定:
示例:volatile 保证有序性
1. 问题:指令重排序导致的结果不一致
代码示例:
class ReorderingExample {
private int value = 0;
private boolean flag = false;
public void writer() {
value = 42; // 写普通变量
flag = true; // 写普通变量
}
public void reader() {
if (flag) { // 读取普通变量
System.out.println("Value: " + value); // 可能输出 0
}
}
}
public class Main {
public static void main(String[] args) {
ReorderingExample example = new ReorderingExample();
Thread writerThread = new Thread(example::writer);
Thread readerThread = new Thread(example::reader);
writerThread.start();
readerThread.start();
}
}
可能输出:
Value: 0
原因:
- 由于指令重排序,
writer
方法中的value = 42
和flag = true
顺序可能颠倒。 reader
方法可能看到flag = true
,但此时value
的写操作尚未完成,读取到的是初始值0
。
2. 使用 volatile 修复有序性问题
将 flag
变量声明为 volatile
,代码如下:
class ReorderingExample {
private int value = 0;
private volatile boolean flag = false;
public void writer() {
value = 42; // 写普通变量
flag = true; // 写 volatile 变量
}
public void reader() {
if (flag) { // 读取 volatile 变量
System.out.println("Value: " + value); // 一定输出 42
}
}
}
保证有序性原因:
- 写入
flag
时,volatile
的写屏障确保value = 42
操作完成后才会执行flag = true
。 - 读取
flag
时,volatile
的读屏障确保在读取flag
之前,所有变量(包括value
)都已从主内存中刷新到线程工作内存。
输出:
Value: 42
3. 内存屏障的作用
volatile
在底层插入了内存屏障,具体如下:
- 写操作内存屏障(Store Barrier):
- 在 volatile变量写入后插入 StoreStore和 StoreLoad屏障:
StoreStore
:保证在volatile
写操作之前的普通写操作不会重排序到volatile
写操作之后。StoreLoad
:防止写后读重排序。
- 读操作内存屏障(Load Barrier):
- 在 volatile变量读取前插入 LoadLoad和 LoadStore屏障:
LoadLoad
:保证在volatile
读操作之后的普通读操作不会被重排序到volatile
读操作之前。LoadStore
:防止读后写重排序。
拓展:volatile 与指令重排序的双重检查锁定
volatile
在 双重检查锁定(DCL,Double-Checked Locking) 中的作用,典型场景是实现线程安全的单例模式:
问题:指令重排序破坏单例
代码示例(有问题的实现):
class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
问题:
-
instance = new Singleton()
是一个非原子操作,分解为以下三步:
- 分配内存。
- 调用构造器初始化对象。
- 将对象引用赋值给
instance
。
- 重排序可能导致步骤 2 和步骤 3 交换顺序:
- 一个线程可能看到
instance
不为null
(步骤 3 完成),但访问的是未初始化的对象(步骤 2 未完成)。
- 一个线程可能看到
解决:volatile 禁止重排序
正确实现:
class Singleton {
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;
}
}
作用:
volatile
禁止instance = new Singleton()
的指令重排序,确保对象在赋值前已完成初始化。- 保证了
Singleton
的线程安全。
总结
- volatile 如何保证有序性?
- 通过内存屏障,禁止
volatile
修饰变量的读写操作与其他操作重排序。 - 在写入
volatile
变量时,确保之前的写操作先于volatile
写操作完成。 - 在读取
volatile
变量时,确保之后的读操作晚于volatile
读操作执行。
- 通过内存屏障,禁止
- 实际场景
- 解决指令重排序问题,如多线程通信、双重检查锁定单例模式。
- 确保多线程环境下的有序执行,避免难以调试的并发问题。