Java中volatile关键字的深度解析
volatile是Java中用于保证变量可见性的关键字,在多线程编程中扮演着重要角色。以下从概念、原理和使用三个方面进行详细解析:
一、核心概念
1. 内存可见性问题
在多线程环境下,每个线程有自己的工作内存(缓存),变量的读写操作可能发生在缓存而非主内存中,导致:
- 脏读:一个线程修改了变量值,但其他线程看不到最新值
- 重排序:编译器或处理器为优化性能可能改变指令执行顺序
2. volatile的作用
- 保证可见性:对
volatile变量的写操作会立即刷新到主内存,读操作会从主内存读取最新值 - 禁止指令重排序:通过内存屏障(Memory Barrier)禁止特定类型的指令重排序
- 不保证原子性:
volatile不具备互斥性,无法保证复合操作(如i++)的原子性
二、底层原理
1. JMM(Java内存模型)
volatile变量的读写遵循JMM的happens-before规则:- 写操作先行发生于读操作:一个线程对
volatile变量的写操作,对后续读取该变量的线程可见 - 传递性:如果A先行发生于B,B先行发生于C,则A先行发生于C
- 写操作先行发生于读操作:一个线程对
2. 内存屏障(Memory Barrier)
JVM会在volatile变量操作前后插入内存屏障,确保:
- 写屏障:强制将写操作结果刷新到主内存
- 读屏障:强制从主内存读取最新值
- 禁止重排序:确保屏障前后的指令不会被重排序
3. 硬件层面
- Lock前缀指令:在x86架构中,对
volatile变量的写操作会生成Lock前缀指令 - 缓存一致性协议:通过MESI协议保证缓存行的一致性
三、使用场景
1. 状态标志(布尔开关)
public class VolatileExample {
private volatile boolean shutdown = false;
public void shutdown() {
shutdown = true; // 写操作立即刷新到主内存
}
public void doWork() {
while (!shutdown) { // 读操作从主内存获取最新值
// 执行任务
}
}
}
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(); // 禁止重排序
}
}
}
return instance;
}
}
3. 与原子类结合使用
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private volatile boolean closed = false;
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
if (!closed) {
count.incrementAndGet(); // 原子操作+可见性
}
}
public void shutdown() {
closed = true; // 立即通知其他线程
}
}
四、深入理解
1. 与synchronized的对比
| 特性 | volatile | synchronized |
|---|---|---|
| 原子性 | 不保证 | 保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 禁止部分重排序 | 禁止指令重排序 |
| 性能 | 开销小 | 开销大(涉及上下文切换) |
| 使用场景 | 状态标志、单次初始化 | 复合操作、临界区保护 |
2. 错误示例:不适合复合操作
public class WrongUsage {
private volatile int count = 0; // 错误示例
public void increment() {
count++; // 非原子操作(读取、修改、写入),仍会导致竞态条件
}
}
3. 正确示例:原子类+volatile
public class CorrectUsage {
private volatile boolean finished = false;
private final AtomicInteger sum = new AtomicInteger(0);
public void add(int value) {
if (!finished) {
sum.addAndGet(value); // 原子操作+可见性
}
}
public void finish() {
finished = true; // 立即通知其他线程
}
}
五、JDK中的应用
-
java.util.concurrent包:AtomicBoolean、AtomicInteger等原子类内部使用volatile保证可见性ConcurrentHashMap中的Node节点使用volatile保证读操作的可见性
-
java.lang.Thread类:Thread类中的stop()方法使用volatile标志实现线程中断
六、总结
-
适用场景:
- 状态标志(如
shutdown、initialized) - 禁止指令重排序(如单例模式的DCL)
- 与原子类配合使用(如
AtomicInteger)
- 状态标志(如
-
不适用场景:
- 复合操作(如i++)
- 需要原子性保证的场景(使用
synchronized或Atomic类)
-
性能考量:
volatile比synchronized更轻量,适合读多写少的场景- 过度使用可能导致性能下降(频繁刷新缓存)
合理使用volatile可以在保证线程安全的同时,避免synchronized带来的性能开销,是多线程编程中的重要工具。