有一回面 Java 岗,我随口问了一句:"volatile 关键字有什么用?"

面试者眼睛都没眨:"保证可见性,禁止指令重排。"

说得好——基本上是标准答案。但我觉得还不够。我接着问:

"那我写一个 volatile int count = 0,然后 10 个线程同时 count++,最后 count 是不是等于 10?"

他犹豫了半天:"……应该是,因为 volatile 保证可见性。"

我当场给了他一个无害的笑。

因为 10 个线程同时 count++,冲这个 volatile 可见性没半毛钱用。volatile 不保证原子性。 下一秒他被问住了。

这个坑 80% 的人都掉过。因为网上的资料把 volatile 讲得玄乎其神,"可见性""有序性"这俩词一排列组合——谁也闹不清真正运作时是怎么回事。

volatile 真正干了两件事——不多,就是两件

第一件:确保变量的值直接从主内存里读、写到主内存里去

这句话简单——但后面藏着计算机系统最底层的两个坑:CPU 缓存编译器优化

你写了一个 boolean flag = true。你觉得它就该立马生效——但在计算机眼里,它可能被塞到 CPU 的 L1 或者 L2 缓存里了。别的 CPU 核心根本看不见你改的东西。

volatile 就是一件硬东西。它告诉编译器和 CPU:对这个变量的读写,每一步都直接走主内存——绕过 CPU 缓存。

这就保证了一个线程修改 volatile 变量后,其他线程立即看到最新值。

但只是值本身。 它不保护你拿这个值去做多步操作。比如 volatile int count; count++——这其实是三步操作:读值、加 1、写值。volatile 保证每一步读和写都能看到最新值——但它不保证这三步是一个原子操作。

第二件:禁止指令重排——解决一个更深的坑

这一件比第一件更难看到——但它是 volatile 最常用的场景。

指令重排是个啥?

编译器为了性能,可能会重新排列你的代码顺序。比如你写了:

// 线程 A
config = newConfig();      // ①
initialized = true;        // ②

// 线程 B
while (!initialized) {}    // ③
config.doSomething();      // ④

你以为 ① 一定在 ② 前面执行——但 CPU 觉得"我先执行 ② 也没啥问题(② 不吃 ① 的结果)"——于是把 ② 提到 ① 前面了。

结果就是:线程 B 看到 initialized = true 了,就去调 config.doSomething(),但 config 还没初始化完——空指针。这也叫"对象逸出"问题。

// volatile 就解决这个:
private volatile boolean initialized = false;

volatile 告诉编译器和 CPU:这个变量前面的代码不准跑到它后面,后面的代码不准跑到它前面。 这就强制 ① 先于 ②——杜绝了对象逸出。

volatile 经典用例:单例模式的双重检查锁

这是面试中 volatile 最常见的使用场景——不知道的话,DCL(双重检查锁)会被面试官怼个底朝天:

public class Singleton {
    private static volatile Singleton instance;  // ← 注意这个 volatile

    public static Singleton getInstance() {
        if (instance == null) {                  // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {          // 第二次检查
                    instance = new Singleton();  // ← 问题出在这一行
                }
            }
        }
        return instance;
    }
}

为什么这里必须用 volatile?

因为 instance = new Singleton() 这一行,JVM 拆成了三步:

① 分配内存空间
② 初始化对象(调用构造方法)
③ 将 instance 引用指向分配好的内存空间

如果没有 volatile,JVM 可能把②和③重排——先把内存空间地址赋给 instance(③),再初始化对象(②)。

这时候,线程 A 在同步块里刚刚执行完③——instance 已经不为 null 了但还没完成初始化。线程 B 过来做第一次检查——发现 instance 不为 null!直接就拿去用了一个半初始化的对象。爆炸。

volatile 禁止重排,保证了②一定在③之前——DCL 才正确。

再深挖一层:volatile 的底层实现

面试官下一步可能会逼你一句:"volatile 在底层是怎么实现的?"

Java 内存模型(JMM)规定,volatile 变量的写操作前后会插入内存屏障

具体来说,通过 lock 前缀指令实现:

  1. 写 volatile 时:JVM 在写操作后插入一个 store-store 屏障 + store-load 屏障。确保当前 CPU 的写缓冲刷新到主内存,并让其他 CPU 的缓存失效。
  2. 读 volatile 时:JVM 在读操作前后插入 load-load 屏障load-store 屏障。确保读操作从主内存拉最新值,并禁止后面的读写被重排到前面。

说得更直白一点——volatile 的代价是:每次读写都穿透 CPU 缓存,直击主内存。 所以 volatile 比普通变量慢——但它保证了你程序的正确性。

如果你能解释到这一步——面试官会问你:"那你有考虑过 volatile 的性能吗?"这问题直接证明了他已经认可你对 volatile 的理解。

面试分三档

第一档:答得出"保证可见性、禁止指令重排"——及格线附近。说明看过面经。

第二档:在第一档基础上,能解释 i++ 不是原子的 + 能用内存屏障说明底层实现 + DCL 中的实际应用——通过线。说明真正用过 volatile、踩过坑。

第三档:在第二档基础上,能主动说:"volatile 保证可见性不保证原子性——这俩是两码事,一个控制缓存一致性、一个控制操作原子性。你把 volatile 用在单线程对多线程的通话场景(比如开关标识),或者与锁配合用——不要想用 volatile 替代锁。"

关键就在最后一句话。 面试官从这句里看到:你知道 volatile 能干什么、不能干什么、边界在哪——你不是背面试题的。你是一个真正用了它的人。

volatile 你踩过什么坑?有没有线上因为 volatile 造成的 bug 让你抓狂过?留言区唠唠。

本文作者:资深 Java 开发老兵,前大厂面试官。不讲八股文,只讲面试官心里在想什么。系列持续更新,关注不迷路。

如果觉得有帮助,点个关注支持一下。

我最近整理了一份《大厂面试老兵资料包》,里面有评分表、高频场景题还有今年新出的 AIGC 面试题,

都是 200 多场面试里一点点攒的,市面上没人这么整理过。

需要的朋友私信我回复「666」,我发你领取方式。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐