为什么需要volatile?先看看并发编程的“坑”
想象一下,你和你的小伙伴共同编辑一份在线文档(共享变量),你修改了内容,但你的小伙伴却看不到最新版本,这会造成什么后果?这就是典型的可见性问题。
在并发编程中,每个线程都有自己的工作内存(相当于CPU缓存),当一个线程修改了共享变量,其他线程不一定能立即看到这个修改。这就像你更新了在线文档,但你的同事仍然看到的是缓存中的旧版本。
除了可见性问题,还有指令重排序的陷阱。编译器和处理器为了优化性能,可能会重新排序指令执行顺序,这在单线程下没问题,但在多线程环境下可能导致意想不到的结果。
volatile的两大核心特性
volatile关键字虽然看起来简单,但它具备两项重要特性,使其成为Java并发编程中解决特定问题的利器。
1. 可见性保证
当一个变量被声明为volatile后,对该变量的任何写操作都会立即刷新到主内存中,而对该变量的读操作都会从主内存中读取。
这就好比有一个严格的图书管理员:每当有人还书(写操作),他立即将书放回正确位置;每当有人借书(读操作),他确保给出的是最新的版本。
public class VolatileExample {
private static volatile boolean flag = false;
public static void main(String[] args) {
Thread writerThread = new Thread(() -> {
try {
Thread.sleep(1000); // 模拟一些工作
flag = true; // 写入volatile变量
System.out.println("标志位已设置为true");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread readerThread = new Thread(() -> {
while (!flag) {
// 循环直到检测到flag变为true
}
System.out.println("检测到标志位变化,线程退出");
});
readerThread.start();
writerThread.start();
}
}
在这个例子中,如果没有volatile,readerThread可能永远检测不到flag的变化,导致无限循环。而使用volatile后,可以确保可见性。
2. 禁止指令重排序
volatile的第二个核心功能是禁止指令重排序。编译器和处理器的重排序优化,在单线程环境下没有问题,但在多线程环境下可能导致诡异的问题。
最经典的例子就是双重检查锁定(DCL)单例模式:
public 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;
}
}
为什么instance需要volatile?因为instance = new Singleton()这行代码包含三个步骤:
- 分配对象内存空间
- 初始化对象
- 将引用指向分配的内存地址
如果没有volatile,步骤2和3可能被重排序,导致其他线程获取到未完全初始化的对象!使用volatile可以防止这种重排序。
volatile的实现原理:底层探秘
现在,让我们深入底层,看看volatile是如何实现这些神奇特性的。
内存屏障:volatile的“守护神”
volatile的关键实现机制是内存屏障(Memory Barrier)。内存屏障是一种CPU指令,用于控制特定操作顺序,就像一道屏障,确保屏障前后的指令不会越过它执行。
JVM在volatile读写操作前后插入内存屏障:
- 在volatile写操作前后:
- 前面插入StoreStore屏障:禁止上面的普通写与volatile写重排序
- 后面插入StoreLoad屏障:禁止volatile写与下面可能的volatile读/写重排序
- 在volatile读操作前后:
- 后面插入LoadLoad屏障:禁止下面的普通读与volatile读重排序
- 后面插入LoadStore屏障:禁止下面的普通写与volatile读重排序
硬件层面的支持:LOCK前缀指令
在x86架构下,volatile的写操作会生成带有LOCK前缀的指令。这个LOCK前缀可不是锁总线那么简单,现代CPU使用缓存一致性协议(如MESI协议)来实现。
当CPU发现要操作的变量被volatile修饰时:
- 会将当前处理器缓存行的数据立即写回主内存
- 这个写回操作会使其他CPU中缓存该内存地址的数据无效
这就像在一个团队会议上,当某人更新了共享文档后,立即通知所有人:“文档已更新,请重新加载!”
volatile的局限性:它不是什么都能做
虽然volatile很强大,但它并不是万能的。最关键的局限性是:volatile不能保证原子性。
什么是原子性?一个操作要么完全执行,要么完全不执行,中间不会被打断。但volatile不能保证复合操作的原子性。
最经典的例子就是i++操作:
public class AtomicityExample {
private static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count++; // 这不是原子操作!
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count++; // 这不是原子操作!
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终结果: " + count); // 可能小于20000
}
}
为什么volatile不能保证原子性?因为count++实际上包含三个步骤:
- 读取count的值
- 将值加1
- 将新值写回count
在多线程环境下,两个线程可能同时读取到相同的值,然后分别加1并写回,导致结果不符合预期。
如果需要保证原子性,应该使用synchronized、Lock或Atomic类。
volatile的应用场景:恰到好处的使用
既然了解了volatile的能力和限制,我们在什么情况下应该使用它呢?
1. 状态标志
最经典的用法是作为一个简单的状态标志:
public class TaskRunner implements Runnable {
private volatile boolean running = true;
public void run() {
while (running) {
// 执行任务
}
}
public void stop() {
running = false;
}
}
这种情况下,使用volatile是完美的,因为只有一个线程修改running标志,多个线程读取它。
2. 一次性安全发布
volatile可以用于安全发布不可变对象:
public class ResourceFactory {
private volatile Resource resource;
public Resource getResource() {
if (resource == null) {
synchronized(this) {
if (resource == null) {
resource = new Resource(); // 安全发布
}
}
}
return resource;
}
}
3. 独立观察模式
定期“发布”观察结果供程序其他部分使用:
public class TemperatureSensor {
private volatile double currentTemperature;
private void senseTemperature() {
while (true) {
// 读取温度传感器
currentTemperature = readSensor();
Thread.sleep(1000);
}
}
public double getTemperature() {
return currentTemperature; // 总是读取最新值
}
}
4. volatile bean模式
在特定情况下,可以将bean的所有成员变量都声明为volatile,但这适用于特定场景。
总结:正确使用volatile的要点
- 保证可见性:一个线程修改volatile变量,其他线程立即可见。
- 保证有序性:通过内存屏障禁止指令重排序。
- 不保证原子性:复合操作(如i++)仍需其他同步机制。
- 更轻量级:相比
synchronized,不会引起线程上下文切换。
volatile关键字是Java并发编程中的重要工具,虽然它不能解决所有并发问题,但在适当的场景下,它是一个简单高效的解决方案。理解其底层原理和适用场景,有助于我们编写更安全、高效的多线程程序。