volatile 这题别只答「可见性」:面试官真正想听的是这条 happens-before

volatile 这题别只答「可见性」:面试官真正想听的是这条 happens-before

很多人答 volatile,开口就是「保证可见性,禁止指令重排」。不能说错,但这两句太像背书了,面试官往下问一句「那为什么 count++ 还是不行」,很容易就断在这里。

把它换成一条可推导的规则会稳很多:对同一个 volatile 变量,一次写入 happens-before 之后任意线程对它的读取。再加上线程内的代码顺序,前面的普通写就能跟着一起被发布出去。

看一个常见的状态发布场景:

java 复制代码
class TaskResult {
    private int value;
    private volatile boolean ready;

    void publish() {
        value = 42;
        ready = true;
    }

    int awaitResult() {
        while (!ready) {
            Thread.onSpinWait();
        }
        return value;
    }
}

当读线程看到 ready == true 时,它不该再看到旧的 value。理由不是「volatile 很神奇」,而是这条链:

text 复制代码
value = 42
  → ready = true(同一线程内的程序顺序)
  → 读取到 ready == true(volatile 写与读)
  → return value

Java 语言规范把这个关系叫 happens-before。前一个动作对后一个动作可见,并且在内存模型的语义上排在前面。

这里有个容易漏掉的前提:读线程得先读到那个 volatile 状态。要是代码先读 value,再看 ready,这条发布链就被你自己拆开了。

为什么 count++ 还是会丢

java 复制代码
class Metrics {
    private volatile int count;

    void record() {
        count++;
    }
}

count++ 不是一个不可分割的动作。它至少包含读旧值、加一、写回新值。

两个线程都读到 10,各自加一再写回 11,最后就少记了一次。volatile 让彼此更容易看到最新写入,但没有把这三个动作捆成一次。

这种场景该用 AtomicInteger.incrementAndGet(),或者用锁把「读、改、写」包起来。面试里可以直接按数据语义选:

  • 只是发布一个状态或引用:volatile 合适。
  • 需要一个数的原子增减:原子类合适。
  • 多个字段必须一起满足约束:锁更直白。

比如库存和已售数要同时更新,哪怕两者都加 volatile 也没有意义。问题不在单个字段看不见,而在这组更新不能被别的线程看见一半。

双重检查锁为什么一定要写 volatile

单例是这题最常见的追问:

java 复制代码
class Config {
    private static volatile Config instance;

    static Config getInstance() {
        if (instance == null) {
            synchronized (Config.class) {
                if (instance == null) {
                    instance = new Config();
                }
            }
        }
        return instance;
    }
}

没有 volatile 时,别只说「可能重排序」。把现象讲出来:另一个线程可能先看到一个非 null 的引用,却还没看到构造阶段写进去的完整状态。

instance 加 volatile 后,构造完成前的写入会随着这次 volatile 写发布;读到非 null 的线程再返回它,看到的是已经发布完成的对象状态。

这不表示 volatile 能替代 synchronized。这里的 synchronized 还在做「只能创建一次」的互斥,volatile 解决的是锁外那次读取能不能安全拿到对象。

面试时我会先问自己这三个问题

不用急着抛术语,先判断操作本身:

  1. 这是单次读写,还是读完还要根据旧值再写?
  2. 读线程有没有一个明确的状态变量,作为发布完成的信号?
  3. 这次修改会不会同时影响其他字段的不变量?

第一类才是 volatile 的主场。第二类决定状态发布能不能成立。第三类一旦回答「会」,基本就该往锁、原子类或并发容器上想了。

本文核对了 Java SE 21《Java Language Specification》第 17.4.4、17.4.5 节:volatile 写与后续读取的 synchronizes-with 关系,以及 happens-before 的传递规则。

相关推荐
weixin_440784117 小时前
【HandlerThread实现原理】
android·java·开发语言
leoZ2317 小时前
本地跑大模型实战(七):llama.cpp 性能调优,让推理更快更省
java·人工智能·spring·生成对抗网络·语言模型·自然语言处理·llama
mubei-1238 小时前
SpringDAO的用法
java·开发语言·数据库
happymagic8 小时前
java spring boot做的jar包程序,如何实现自动运行启动
java·运维·服务器·spring boot·jar
都叫我大帅哥11 小时前
给 Java 程序员的 CSRF 攻击详解:从原理到防御实战
java
江南十四行11 小时前
Spring框架核心(上)——IoC控制反转与DI依赖注入详解
java·后端·spring
fb_1234512 小时前
Linux磁盘分区从入门到实操:MBR_GPT全解析+分区工具实战指南
java·linux·gpt
码智社13 小时前
JSON 零基础到资深全体系教程
java·json
wgego14 小时前
基础的反序列化一些总结(php和java)
java·开发语言·笔记
IKUN家族14 小时前
Spring MVC(四)
java·spring·mvc