volatile 变量读写时插入了哪些内存屏障
volatile 关键字在 java 中通过插入特定的内存屏障(memory barrier)来保证可见性和有序性。具体做法是:读操作前插入 loadload 和 loadstore 屏障,写操作后插入 storestore 和 storeload 屏障。这样做会强制 cpu 刷新缓存行到主存并重新加载最新值,同时禁止编译器进行指令重排序。

下面是一个验证 volatile 屏障行为的完整程序。它定义了一个共享的 volatile int 变量和一个普通 int 变量,通过线程写入和读取来观察顺序。代码展示了如何触发这些屏障。
public class VolatileBarrierDemo {
// volatile 变量,触发屏障
private static volatile int v = 0;
// 普通变量,无屏障保护
private static int normal = 0;
public static void main(String[] args) throws InterruptedException {
Thread writer = new Thread(() -> {
normal = 1; // 步骤 1: 写普通变量
v = 1; // 步骤 2: 写 volatile 变量 (触发 StoreStore, StoreLoad)
});
Thread reader = new Thread(() -> {
int r1 = v; // 步骤 3: 读 volatile 变量 (触发 LoadLoad, LoadStore)
int r2 = normal; // 步骤 4: 读普通变量
System.out.println("r1: " + r1 + ", r2: " + r2);
});
writer.start();
reader.start();
writer.join();
reader.join();
}
}
预期结果:由于 v 的写操作发生在读操作之前,且 volatile 保证了可见性,r1 通常为 1。r2 的值取决于调度,可能为 0 或 1,因为 normal 没有屏障保护,可能与 v 发生重排序。
内存屏障如何防止指令重排序
内存屏障通过限制编译器和处理器的重排序能力来确保代码执行的有序性。对于写操作,StoreStore 屏障确保前面的普通写操作不会被重排序到 volatile 写之后;StoreLoad 屏障确保 volatile 写之后的所有操作不会被重排序到它之前。对于读操作,LoadLoad 屏障确保后面的读操作不会被重排序到 volatile 读之前;LoadStore 屏障确保 volatile 读之后的写操作不会被重排序到它之前。

以下代码演示了双重检查锁定(DCL)模式中,如果没有 volatile,指令重排序可能导致实例未完全初始化就被其他线程使用。加入 volatile 后,内存屏障阻止了 new Singleton() 的内部指令与赋值语句的重排序。
public class Singleton {
// 加上 volatile 防止指令重排序导致的 DCL 失效
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 这里涉及三步:分配内存、初始化、赋值
// volatile 写操作插入 StoreStore 和 StoreLoad 屏障
}
}
}
return instance;
}
}
预期结果:在多线程环境下,getInstance() 始终返回一个完全初始化的 Singleton 对象。如果没有 volatile,其他线程可能拿到一个引用但对象字段尚未初始化的“半初始化”状态。
volatile 写操作后的 StoreLoad 屏障有什么作用
StoreLoad 屏障是四种屏障中最昂贵的一种,它确保 volatile 写之前的所有内存操作(包括普通读写)都已完成,并且其结果对后续的所有内存操作可见。它主要用于隔离不同的临界区,防止跨临界区的指令重排序。在 x86 架构下,虽然硬件层面通常不执行完整的 StoreLoad 屏障(因为 x86 的强内存模型),但在 Java 内存模型(JMM)层面,它必须被实现,通常通过调用汇编指令或库函数来实现全序一致性。

以下代码展示了在并发计数器场景中,volatile 的 StoreLoad 屏障如何确保计数更新的原子性感知。虽然 volatile 本身不保证复合操作的原子性,但它确保了每次写操作对其他线程的即时可见性,配合 StoreLoad 屏障,可以防止后续的读操作读到旧值。
public class Counter {
private volatile long count = 0;
public void increment() {
// 注意:volatile 不保证 i++ 的原子性,这里仅展示屏障效果
// 实际原子操作应使用 AtomicLong
long temp = count;
count = temp + 1; // 写 volatile 变量,触发 StoreLoad 屏障
}
public long getCount() {
return count; // 读 volatile 变量,触发 LoadLoad 屏障
}
}
预期结果:在单线程下,count 正确递增。在多线程下,getCount() 总是能看到最新的 count 值,尽管 increment() 本身不是原子的,可能存在丢失更新的情况,但可见性是绝对保证的。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











