在java后端开发的多线程编程场景中,volatile是保证变量线程安全的重要关键字之一,它的作用主要围绕多线程下的内存交互和指令执行顺序展开,能够解决部分并发场景下的数据不一致问题。

volatile的核心作用
1. 保证内存可见性
在java内存模型中,每个线程有自己的工作内存,线程操作变量时会从主内存拷贝副本到工作内存,操作完成后再写回主内存。普通变量修改后,其他线程无法立即感知到变化,而volatile修饰的变量会有特殊规则:线程修改volatile变量后会立即刷新到主内存,同时其他线程工作内存中该变量的副本会失效,必须重新从主内存读取,以此保证所有线程看到的变量值是一致的。
以下是一个简单的可见性示例:
// 未加volatile的变量,线程可能无法感知到修改
private static boolean flag = false;
// 加volatile的变量,修改后其他线程可立即感知
private static volatile boolean volatileFlag = false;
public static void main(String[] args) throws InterruptedException {
// 测试普通变量
Thread t1 = new Thread(() -> {
while (!flag) {
// 空循环,等待flag被修改
}
System.out.println("普通变量flag被修改,线程结束");
});
t1.start();
Thread.sleep(1000);
flag = true;
// 这里t1可能不会结束,因为看不到flag的修改
// 测试volatile变量
Thread t2 = new Thread(() -> {
while (!volatileFlag) {
// 空循环,等待volatileFlag被修改
}
System.out.println("volatile变量volatileFlag被修改,线程结束");
});
t2.start();
Thread.sleep(1000);
volatileFlag = true;
// 这里t2一定会结束,因为volatile保证了可见性
}
2. 禁止指令重排
为了优化执行效率,编译器和处理器可能会对指令进行重排序,只要重排序后单线程下的执行结果不变,重排序就是允许的。但在多线程场景下,指令重排可能导致逻辑错误。volatile会通过内存屏障禁止特定类型的指令重排:
- 在volatile写操作前插入StoreStore屏障,禁止上面的普通写和volatile写重排
- 在volatile写操作后插入StoreLoad屏障,禁止volatile写和后面的读操作重排
- 在volatile读操作后插入LoadLoad屏障,禁止volatile读和后面的普通读重排
- 在volatile读操作后插入LoadStore屏障,禁止volatile读和后面的普通写重排
典型的双重检查锁单例模式中就需要用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) {
// 如果不加volatile,这里可能发生指令重排:
// 1. 分配内存 2. 指向内存地址 3. 初始化对象
// 若2和3重排,其他线程可能拿到未初始化的对象
instance = new Singleton();
}
}
}
return instance;
}
}
volatile的适用场景
volatile并不是万能的,它只适合以下两类场景:
- 变量的写操作不依赖当前值,或者只有单个线程修改变量值,比如状态标记位,像上面的flag示例,只有主线程修改flag,其他线程只读
- 变量不参与不变式约束,也就是变量不需要和其他变量共同配合来完成某个逻辑判断
volatile和synchronized的区别
很多开发者会混淆两者的作用,它们的核心区别如下:
| 对比维度 | volatile | synchronized |
|---|---|---|
| 原子性 | 不保证复合操作原子性,比如volatile++不是原子操作 | 保证代码块的原子性,同一时间只有一个线程执行 |
| 可见性 | 保证 | 保证,因为解锁前会刷新变量到主内存 |
| 有序性 | 禁止指令重排 | 保证,因为同一时间只有一个线程执行,相当于单线程环境 |
| 性能 | 更轻量,不会阻塞线程 | 重量级,会阻塞线程,性能开销更大 |
如果需要对变量的复合操作保证原子性,比如计数器的自增操作,就不能只用volatile,需要配合synchronized或者使用AtomicInteger等原子类来实现。