Java面试之volatile关键字的内存可见性

volatile通过强制主内存读写和内存屏障保证可见性,但不保证原子性;它建立happens-before关系,使写前操作对后续读线程可见,依赖实际读写动作而非自动同步。

volatile 为什么能保证内存可见性

因为 volatile 强制线程每次读取变量都从主内存重新加载,每次写入都立即刷新回主内存,绕过了 CPU 缓存的本地副本。JVM 在生成字节码时,会对 volatile 读写插入内存屏障(Memory Barrier):读操作

前加 LoadLoadLoadStore,写操作后加 StoreStoreStoreLoad,阻止指令重排序并确保可见性传播。

注意:它不提供原子性——i++ 这种复合操作即使作用于 volatile int i,依然可能丢失更新。

volatile 不能替代 synchronized 的典型场景

当需要「读-改-写」原子性时,volatile 失效。比如计数器自增、状态标志配合业务逻辑判断后再操作等。

  • volatile boolean flag = false; 可用于通知线程退出,但若写成 if (flag) doSomething(); flag = true;,就存在竞态——两线程同时通过 if 判断后都执行 doSomething()
  • volatile int count; 无法安全执行 count++,因为底层是 getfield → iconst_1 → iadd → putfield 三步,中间可能被其他线程打断
  • 对象引用虽用 volatile 修饰,但其内部字段修改仍不具可见性,例如 volatile List list = new ArrayList();,后续 list.add("a") 不会触发对其他线程的可见保障

volatile 与 happens-before 规则的直接关联

JMM 中,对一个 volatile 变量的写操作,happens-before 于任意后续对该变量的读操作。这是唯一一条由 Java 语言规范明确定义的、不依赖锁的 happens-before 关系。

这意味着:如果线程 A 写了 volatile boolean ready = true;,线程 B 后续读到 ready == true,那么线程 A 在写 ready 之前的所有内存操作(包括非 volatile 字段赋值),对线程 B 也一定可见。

class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;           // 非 volatile 写
        flag = true;     // volatile 写 → 建立 happens-before 边界
    }

    public void reader() {
        if (flag) {      // volatile 读
            System.out.println(a); // 此处一定能打印 1
        }
    }
}

常见误判:volatile 能防止指令重排序,但不等于“顺序执行”

它只禁止特定类型的重排序(编译器和处理器不会把 volatile 读/写与前后某些操作乱序),但不保证多线程下所有语句的全局执行顺序一致。比如两个线程分别写不同的 volatile 变量,它们之间的相对顺序对第三方线程而言仍是不确定的。

真正容易被忽略的是:volatile 的可见性保障,依赖于「至少有一个线程执行了 volatile 写,另一个线程执行了 volatile 读」。如果读线程从未读取该变量,或者写线程写完后读线程才启动且未触发 volatile 读,那之前的写操作对其不可见——不是“一写全知”,而是“读到才同步”。