volatile 是 Java 并发编程中最容易被误解的关键字之一。很多人简单地认为它只是"保证变量可见性",但实际上它的作用远不止于此。从 JVM 层面看,volatile 实际上是通过插入内存屏障(Memory Barrier)来实现两个核心语义:
可见性保证:当一个线程修改 volatile 变量时,修改后的值会立即被写回主内存,而不是仅停留在工作内存。同时,其他线程读取该变量时会强制从主内存重新加载最新值。
禁止指令重排序:JVM 和 CPU 为了优化性能会对指令进行重排序,但 volatile 变量的读写操作不会被重排序到这些操作之前或之后,形成了一种"屏障"效果。
重要提示:volatile 并不保证原子性!这是它与 synchronized 最本质的区别。比如 volatile int i = 0; i++ 这样的操作在多线程下仍然是不安全的。
Java 内存模型(JMM)规定了所有变量都存储在主内存中,每个线程有自己的工作内存。工作内存是主内存的副本,线程对变量的操作都在工作内存中进行。这就导致了经典的可见性问题:
java复制// 示例1:可见性问题
public class VisibilityIssue {
boolean running = true; // 非volatile
void work() {
while (running) { /* 工作循环 */ }
}
void stop() { running = false; }
}
在这个例子中,即使线程B调用了stop(),线程A的work()方法可能仍然看不到running的修改,导致无限循环。
当变量声明为 volatile 时:
这个机制是通过以下方式实现的:
java复制// 修正后的正确版本
public class VisibilitySolution {
volatile boolean running = true; // 添加volatile
void work() { /* 同前 */ }
void stop() { running = false; }
}
现代处理器和编译器为了优化性能,会对指令进行重排序。例如:
java复制// 示例2:指令重排序问题
class ReorderingExample {
int x = 0;
boolean initialized = false; // 非volatile
void init() {
x = 42; // 写1
initialized = true; // 写2
}
void use() {
if (initialized) { // 读1
System.out.println(x); // 读2
}
}
}
由于指令重排序,实际执行顺序可能是写2→写1,导致use()方法可能输出0而不是42。
volatile 通过内存屏障限制重排序:
java复制// 修正后的版本
class ReorderingSolution {
int x = 0;
volatile boolean initialized = false; // 添加volatile
void init() {
x = 42;
initialized = true; // 写屏障会确保x=42在这之前完成
}
void use() {
if (initialized) { // 读屏障会确保后续操作不会重排序到之前
System.out.println(x);
}
}
}
| 特性 | volatile | synchronized |
|---|---|---|
| 原子性 | 不保证(单次读写除外) | 保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 有限保证(仅volatile变量相关) | 完全保证 |
| 适用场景 | 状态标志、一次性安全发布 | 复合操作、临界区保护 |
| 性能开销 | 较低 | 较高(涉及锁竞争和上下文切换) |
适合 volatile 的场景:
不适合 volatile 的场景:
java复制// 示例3:双重检查锁定模式
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 可以替代锁
误区二:过度使用 volatile
误区三:认为 volatile 数组的元素也具有 volatile 语义
状态标志:当变量只是作为简单的状态标志(如开关)时,优先考虑 volatile
java复制class Worker {
volatile boolean shutdownRequested;
void shutdown() { shutdownRequested = true; }
void doWork() {
while (!shutdownRequested) {
// 执行任务
}
}
}
一次性发布:当对象构造完成后需要安全发布时,使用 volatile
java复制class ResourceHolder {
private volatile Resource resource;
public Resource getResource() {
Resource res = resource;
if (res == null) {
synchronized(this) {
res = resource;
if (res == null) {
res = resource = new Resource();
}
}
}
return res;
}
}
性能敏感场景:在高度竞争的环境下,考虑使用 Atomic 类代替 volatile
java复制// 使用AtomicInteger代替volatile int
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet(); // 真正的原子操作
在 HotSpot JVM 中,volatile 的实现依赖于:
内存屏障插入:
缓存一致性协议:
在以下测试环境:
结果对比:
| 访问类型 | 耗时(ms) |
|---|---|
| 普通变量 | 120 |
| volatile 变量 | 380 |
| synchronized | 4200 |
实际测试发现:volatile 的读性能接近普通变量,写性能有3倍左右开销,但远低于 synchronized
java复制class VolatileData {
@sun.misc.Contended // 防止伪共享
volatile long value1;
@sun.misc.Contended
volatile long value2;
}
java复制class VarHandleExample {
private static final VarHandle HANDLE;
private int value;
static {
try {
HANDLE = MethodHandles.lookup()
.findVarHandle(VarHandleExample.class, "value", int.class);
} catch (Exception e) {
throw new Error(e);
}
}
void atomicIncrement() {
HANDLE.getAndAdd(this, 1);
}
}
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 修改不被其他线程看到 | 忘记声明 volatile | 添加 volatile 关键字 |
| 复合操作结果不正确 | 误用 volatile 代替锁 | 改用 synchronized 或 AtomicXxx |
| 性能异常下降 | volatile 变量频繁写入 | 减少写操作或改用其他同步方式 |
| 偶尔出现异常值 | 伪共享问题 | 使用填充或 @Contended 注解 |
java复制@JCStressTest
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING)
@State
public class VolatileTest {
int x;
volatile boolean v;
@Actor
void writer() {
x = 1;
v = true;
}
@Actor
void reader(IntResult1 r) {
if (v) {
r.r1 = x;
}
}
}
在实际项目中,我发现 volatile 最适合的场景是那些"写入不频繁但读取频繁"的状态标志。曾经在一个高频交易系统中,我们使用 volatile 作为心跳标志,相比 synchronized 实现了纳秒级的延迟降低。但要注意,任何并发工具都应该基于实际压力测试结果来选择,而不是单纯的理论判断。