volatile 是 Java 中一个轻量级的同步关键字,主要用于修饰变量。它的核心作用是保证可见性 和禁止指令重排序 ,但不能保证原子性。
一、volatile 的两大作用
1. 保证可见性
- 问题背景:在 Java 内存模型(JMM)中,每个线程有自己的工作内存(缓存),主内存中的变量可能被线程缓存到本地。当一个线程修改了共享变量后,其他线程无法立刻看到这个修改,导致数据不一致。
volatile解决方案 :
被volatile修饰的变量,写操作 会立即刷新到主内存;读操作 会直接从主内存读取(跳过本地缓存)。
这样,一个线程修改了volatile变量,其他线程能马上看到最新值。
java
// 示例:volatile 保证可见性,用于线程停止标志
class Runner {
private volatile boolean running = true; // 不加 volatile,子线程可能永远看不到修改
public void stop() { running = false; }
public void run() {
while (running) {
// 执行任务
}
}
}
2. 禁止指令重排序(保证有序性)
- 问题背景:JVM 和 CPU 为了优化性能,可能会对指令进行重排序(在不改变单线程执行结果的前提下)。在多线程环境下,重排序可能导致意想不到的错误。
volatile解决方案 :
在volatile变量的读写操作前后插入内存屏障(Memory Barrier),禁止编译器 / CPU 对相关指令重新排序。
典型应用:双重检查锁定(Double-Checked Locking)单例模式。
java
// 错误的单例(不加 volatile,可能返回未完全初始化的对象)
public class Singleton {
private static volatile Singleton instance; // 必须 volatile
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 实际分三步:1.分配内存 2.初始化 3.引用指向内存
} // volatile 禁止第 2 和 3 步的重排序,防止其他线程看到空引用但未初始化的对象
}
}
return instance;
}
}
二、volatile 不能保证原子性
为什么不能保证原子性?
原子性是指一个或多个操作要么全部执行且不被中断,要么全不执行。
volatile 只能保证单个读 / 写操作 的原子性(例如读一个 long 或写一个 long 是原子的),但复合操作 (如 i++)不是原子的:
i++ 包含"读 - 改 - 写"三步,在多线程下即使 i 是 volatile,也会发生丢失更新的问题。
java
volatile int count = 0;
// 两个线程各执行 10000 次 count++,最终结果可能小于 20000
void increment() {
count++; // 1.读 count 到寄存器 2.加1 3.写回内存 ------ 不是原子操作
}
如何实现原子操作?
- 使用
synchronized或ReentrantLock对整个复合操作加锁。 - 使用
java.util.concurrent.atomic包下的原子类,如AtomicInteger(基于 CAS 实现,保证原子性)。
java
AtomicInteger atomicCount = new AtomicInteger(0);
atomicCount.incrementAndGet(); // 原子自增
三、volatile 与 synchronized 对比
| 特性 | volatile |
synchronized |
|---|---|---|
| 作用 | 保证可见性、有序性(禁止重排序) | 保证原子性、可见性、有序性(通过锁的互斥) |
| 是否阻塞 | 不阻塞线程 | 阻塞其他竞争锁的线程 |
| 适用对象 | 只能修饰变量 | 修饰方法、代码块 |
| 原子性保证 | 不能保证复合操作的原子性 | 能保证代码块内的操作原子执行 |
| 性能 | 比加锁轻量,读写开销小 | 较重(但现代 JVM 已优化) |
四、典型使用场景
1. 状态标志(开关)
java
volatile boolean shutdownRequested = false;
// 线程1: shutdownRequested = true;
// 线程2: while (!shutdownRequested) { ... }
2. 双重检查锁定的单例模式(如前述)
3. 读操作远多于写操作的"一写多读"变量
例如统计配置参数、温度传感器数值等,只有一个线程更新,多个线程读取,用 volatile 即可保证可见性,无需加锁。
总结
volatile的作用 :保证变量在多线程间的可见性 以及禁止指令重排序。- 不能保证原子性 :对于
++、--或其他复合操作,仍需加锁或使用原子类。 - 选择建议 :如果只是需要某个变量的修改对其他线程立即可见,且操作本身是原子的(例如赋值
a = 1,或者boolean标志位),用volatile;如果需要复合操作的原子性,用synchronized或Lock。