Appearance
volatile 的作用与边界:可见性、有序性,但不保证复合操作原子性
volatile 是 Java 并发里非常常见、也非常容易被误用的关键字。很多人记住了它“线程可见”,但不一定真的理解它在什么场景下有用,什么场景下完全不够。
先说结论
volatile 主要解决两个问题:
- 保证变量修改对其他线程可见
- 在一定范围内禁止特定的指令重排序
但它不能单独解决这类问题:
i++count = count + 1- 先判断再修改的复合逻辑
因为这些问题本质上需要的是更强的原子性保证,而不是只有可见性。
什么是内存可见性
理解 volatile,离不开 Java 内存模型,也就是 JMM。
可以先用一个更容易理解的方式来看:
- 共享变量最终存放在主内存中
- 每个线程执行时,会把自己要用到的数据读到工作内存中
- 线程对变量的读写,很多时候发生在自己的工作内存副本上
这就带来一个问题:
一个线程已经修改了共享变量,但另一个线程读到的仍然是旧值。
这时我们就说:这个变量在多个线程之间“不可见”。
而 volatile 的作用,就是让这个变量在多个线程之间的修改更快地对彼此可见。
并发里常说的三个关键词
讨论 volatile 时,经常会同时提到三个概念:
1. 原子性
一个操作要么全部执行成功,要么完全不执行,不会只做一半。
例如:
- 读一个
int - 给一个
int赋值
这种基础读写通常可以看作原子操作。
但像下面这种就不是:
java
count++;因为它实际至少包含:
- 读取
count - 计算
count + 1 - 把新值写回去
多线程同时执行时,很容易丢失更新。
2. 可见性
一个线程修改了共享变量后,其他线程能够及时看到这个新值。
volatile 可以解决这个问题。
3. 有序性
为了优化性能,编译器和处理器可能会对指令进行重排序。
Java 内存模型允许一定范围内的重排序,但前提是:
- 不能改变单线程语义
- 不能破坏 JMM 规定的 happens-before 关系
volatile 可以在一定程度上阻止与它相关的危险重排序。
volatile 能做什么
保证可见性
当一个变量被声明为 volatile 后:
- 线程写入这个变量时,会把新值刷新到主内存
- 其他线程读取这个变量时,会优先从主内存中读最新值
一个典型场景是“停止标记”:
java
public class TaskRunner {
private volatile boolean running = true;
public void stop() {
running = false;
}
public void run() {
while (running) {
// do something
}
}
}这里如果 running 不加 volatile,线程可能一直读到自己工作内存里的旧值,从而无法及时退出循环。
保证一定的有序性
volatile 还常见于“双重检查锁”的场景:
java
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}这里 volatile 的核心意义之一,就是避免对象初始化过程中的重排序问题。
volatile 不能做什么
不能保证复合操作的原子性
下面这种写法仍然是有问题的:
java
private volatile int count = 0;
public void increment() {
count++;
}即使 count 是 volatile,也不能避免并发下的丢失更新。
如果要解决这种问题,通常应该考虑:
synchronizedReentrantLockAtomicIntegerLongAdder
实际开发里怎么判断该不该用 volatile
可以用一个很朴素的判断标准:
适合用 volatile 的场景
- 一个线程写,多个线程读
- 变量本身代表状态位、开关、标记
- 不依赖当前值参与复合计算
- 需要一定的可见性和有序性保证
不适合只靠 volatile 的场景
- 计数器累加
- 复合读写逻辑
- 先判断再更新
- 需要严格线程安全的共享对象状态修改
一句话总结
volatile 很有用,但它不是轻量版锁。
更准确地说:
- 它擅长解决“看不见”和“顺序被打乱”的问题
- 它不擅长解决“多个线程一起改同一份数据”的问题
如果你面对的是共享状态的复合修改,第一反应不应该是 volatile,而应该先想:这里是不是需要真正的同步机制。