在 Java 并发编程中,volatile 是一个看似简单却暗藏玄机的关键字。我第一次在线上环境遇到 volatile 的问题是在一个股票行情推送系统里——某个标志位在多个线程间始终无法及时同步,导致行情推送延迟。这个经历让我深刻认识到理解 volatile 的三大特性:内存可见性、禁止指令重排序,以及它与 synchronized 的本质区别。
volatile 的底层实现依赖于 JVM 的内存屏障(Memory Barrier)机制。当写一个 volatile 变量时,JVM 会在写操作后插入 StoreLoad 屏障,强制将工作内存中的修改立即刷新到主内存;读操作前会插入 LoadLoad 屏障,确保每次读取都从主内存获取最新值。这种机制保证了:
重要提示:volatile 不能保证复合操作的原子性。比如 volatile int i = 0; i++ 这样的操作在多线程下仍然是不安全的。
要理解 volatile 的可见性,必须先掌握 JMM 的三个关键概念:
在没有同步措施的情况下,线程A修改了共享变量,线程B可能永远看不到这个修改。我在电商促销系统里就遇到过这样的案例:一个标记促销是否结束的 boolean 变量没有声明为 volatile,导致部分用户线程在促销结束后仍然能下单。
JMM 通过 happens-before 规则定义内存可见性。对于 volatile 变量:
这个特性在双重检查锁定(DCL)单例模式中至关重要:
java复制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;
}
}
如果没有 volatile,其他线程可能看到未初始化完成的对象,因为 new Singleton() 这个操作可能被重排序为:
现代处理器为了提高性能,会采用以下优化策略:
这些优化会导致程序执行顺序与代码顺序不一致。volatile 通过插入特定类型的内存屏障来限制这种重排序:
| 屏障类型 | 作用描述 | 对应 volatile 操作 |
|---|---|---|
| LoadLoad | 禁止读-读重排序 | volatile 读之后 |
| LoadStore | 禁止读-写重排序 | volatile 读之后 |
| StoreStore | 禁止写-写重排序 | volatile 写之前 |
| StoreLoad | 禁止写-读重排序(最重量级屏障) | volatile 写之后 |
考虑以下代码:
java复制int x = 0;
volatile boolean ready = false;
// 线程A
x = 42; // 普通写
ready = true; // volatile 写
// 线程B
if (ready) { // volatile 读
System.out.println(x); // 普通读
}
由于 volatile 的内存屏障:
x=42 不会被重排序到 ready=true 之后ready==true 时,一定能看到 x==42| 特性 | volatile | synchronized |
|---|---|---|
| 原子性 | 仅保证单次读/写的原子性 | 保证代码块/方法的原子性 |
| 可见性 | 立即可见 | 退出同步块时保证可见 |
| 有序性 | 限制指令重排序 | 限制指令重排序 |
| 阻塞机制 | 非阻塞 | 阻塞(获取锁失败时) |
| 适用场景 | 状态标志、DCL等简单同步 | 复杂操作的原子性保证 |
| 性能开销 | 较低(仅内存屏障) | 较高(涉及锁竞争) |
根据我的经验,选择依据应该是:
使用 volatile 当且仅当:
必须使用 synchronized 的情况:
典型的 volatile 使用场景包括:
我曾见过团队尝试用 volatile 实现计数器:
java复制volatile int count = 0;
void increment() {
count++; // 危险操作!
}
这实际上是个典型的原子性问题。正确的做法应该是:
java复制AtomicInteger count = new AtomicInteger(0);
void increment() {
count.incrementAndGet();
}
虽然 volatile 比 synchronized 轻量,但不当使用仍会影响性能:
java复制@sun.misc.Contended
class VolatileHolder {
volatile long value;
}
当怀疑 volatile 相关问题时:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly 查看汇编指令jstack 检查线程状态我在排查一个分布式锁问题时,就是通过查看汇编指令发现 volatile 写屏障缺失,最终定位到是 JIT 编译器的 bug。
随着 Java 版本演进,volatile 的实现也在优化:
Java 9+ 的改进:
ARM 架构的特殊处理:
与 VarHandle 的配合:
java复制private static final VarHandle COUNT_HANDLE;
static {
try {
COUNT_HANDLE = MethodHandles.lookup()
.findVarHandle(MyClass.class, "count", int.class);
} catch (Exception e) { /* ... */ }
}
void safeIncrement() {
int oldVal;
do {
oldVal = (int) COUNT_HANDLE.getVolatile(this);
} while (!COUNT_HANDLE.compareAndSet(this, oldVal, oldVal + 1));
}
这些新特性让 volatile 在保持语义的同时,能获得更好的性能表现。