volatile 关键字详解
一、volatile 的作用
volatile 是 Java 中用于确保多线程环境下共享变量可见性和有序性的关键字。 它通过以下两种机制实现:
- 禁止指令重排序(通过内存屏障)。
- 强制读写直接操作主内存(而非线程的工作内存)。
二、核心特性
1. 可见性(Visibility)
-
问题:普通变量可能被线程缓存在工作内存中,导致其他线程无法及时看到修改。
-
volatile 的解决方案:
- 写操作:立即将工作内存的值刷新到主内存。
- 读操作:每次直接从主内存读取最新值。
示例:
public class VisibilityDemo {
private volatile boolean flag = false;
public void start() {
new Thread(() -> {
while (!flag) {} // 能及时感知 flag 变化
System.out.println("Flag is true");
}).start();
new Thread(() -> {
flag = true; // 修改后对其他线程可见
}).start();
}
}
2. 有序性(Ordering)
- 问题:编译器或处理器可能对指令重排序,导致多线程执行结果不符合预期。
- volatile 的解决方案: 通过插入内存屏障(Memory Barrier),禁止指令重排序。
内存屏障类型:
| 屏障类型 | 作用 |
|---|---|
StoreStore | 禁止上方普通写与下方 volatile 写重排序。 |
StoreLoad | 禁止 volatile 写与后续 volatile 读/写重排序(开销最大)。 |
LoadLoad | 禁止下方普通读与上方 volatile 读重排序。 |
LoadStore | 禁止下方普通写与上方 volatile 读重排序。 |
三、volatile 的典型使用场景
1. 状态标志位
用于控制线程启停的标记变量:
public class Worker implements Runnable {
private volatile boolean running = true;
public void stop() {
running = false;
}
@Override
public void run() {
while (running) { /* 执行任务 */ }
}
}
2. 双重检查锁定(DCL 单例模式)
解决单例对象初始化时的指令重排序问题:
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 无 volatile 时可能返回未初始化对象
}
}
}
return instance;
}
}
- 无
volatile的问题:new Singleton()可能被重排序为“分配内存 → 返回引用 → 初始化对象”,导致其他线程获取未初始化的实例。
3. 一次性发布(One-Time Safe Publication)
保证对象初始化完成后才对其他线程可见:
public class ResourceHolder {
private volatile Resource resource;
public Resource getResource() {
if (resource == null) {
synchronized (this) {
if (resource == null) {
resource = new Resource(); // 安全发布
}
}
}
return resource;
}
}
四、volatile 的局限性
1. 不保证原子性
-
问题:
volatile无法保证复合操作(如i++)的原子性。 -
示例:
public class Counter { private volatile int count = 0; public void increment() { count++; // 非原子操作(实际为 read-modify-write) } }- 即使
count是volatile的,多线程并发调用increment()仍会导致计数错误。
- 即使
2. 替代方案
- 原子类(如
AtomicInteger):通过 CAS 实现原子操作。 - 锁(如
synchronized):保护临界区。
五、volatile 的实现原理
1. 内存语义
-
写操作:
- 修改工作内存中的值。
- 强制刷新到主内存(插入
StoreStore和StoreLoad屏障)。
-
读操作:
- 从主内存重新加载最新值。
- 插入
LoadLoad和LoadStore屏障。
2. 底层实现
- JVM 层面:通过内存屏障指令(如 x86 的
mfence)实现。 - 硬件层面:依赖 CPU 的缓存一致性协议(如 MESI)。
六、volatile vs synchronized
| 特性 | volatile | synchronized |
|---|---|---|
| 原子性 | 不保证 | 保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 部分保证(禁止重排序) | 完全保证 |
| 阻塞 | 无 | 可能阻塞线程 |
| 适用场景 | 状态标志、DCL 单例 | 临界区保护、复合操作 |
| 性能开销 | 低 | 较高(但 JVM 已优化) |
七、总结
1. 使用场景
- 需要多线程可见性(如状态标志)。
- 需要禁止指令重排序(如 DCL 单例模式)。
2. 注意事项
- 不适用于复合操作(如
i++)。 - 不能替代锁或原子类。
3. 最佳实践
- 简单状态控制:优先使用
volatile。 - 复合操作:使用锁或原子类。
- 安全发布对象:结合
volatile与 DCL 模式。
通过合理使用 volatile,可以在不引入锁的情况下解决多线程的可见性和有序性问题,但需明确其适用边界,避免误用导致并发 Bug。