← 每日面试题 学习看板

volatile是如何保证可见性和有序性的?

面试题/00java/02Java 并发编程/04volatile关键字/volatile是如何保证可见性和有序性的?.md

面试记录 2026-09-26

题目:volatile 是如何保证可见性和有序性的?

难度:medium

标签:java/volatile, general/knowledge

评分:⭐⭐⭐☆☆

我的回答

volatile是 Java 的一个核心的内部自定义的关键字。它的语义就是保证可见性,内核其实是保证内核的可见性。然后它实现可见性的原理就是,它在被这个关键字标注的对象更新的时候,它是一定要从内存中拿最新的对象的,确保这个对象永远是拿到的是最新的,和内核中是一样的,不会出现线程上的问题,拿到对象一定是新的,更新后的。然后它保证有序性是它在修饰的时候会在对象前后加上它的读写屏障,确保这个对象不会被 Java 的优化器优化更新到不符合预期的位置。当然这个更新的位置本身是比较困难的,就是说更新的可能性比较低,但是是有这种可能性的。就是说在编译的执行的过程中,把一个两个相互可能它觉得不相干的,或者有优化可能性的对象的位置,会去进行调整。这时候可能会影响整个程序的逻辑,产生不可预期的成结果。Volatile 可以保证它的有序,不会被它重新编译更新位置。

点评

优点:抓住两个核心语义(可见性+有序性);提到"读写屏障"关键点;理解指令重排可能破坏程序语义

不足:表述混乱;未说清可见性底层机制(MESI + lock 前缀指令 + 刷主存);未列举 4 种内存屏障;未提 i++ 经典反例;未说适用场景(DCL、状态标志位)

补充:volatile = 编译器禁止重排 + CPU 加 lock 前缀刷缓存;4 种屏障 StoreStore/StoreLoad/LoadLoad/LoadStore;三大特性:可见性、有序性、不保证原子性

完整讲解

第一部分:知识重点剖析

核心考点定位:能否说清 volatile 的两大特性底层机制(MESI + 内存屏障),以及"不保证原子性"的边界。

知识点拆解:

  1. 可见性的两层机制:
  1. 有序性的内存屏障(JMM 定义 4 种):
  1. 不保证原子性:volatile 只保证每次单条读/写操作的可见性,i++ 是三步(读、加、写),两个线程同时执行会丢更新——这是 volatile 最经典的反例
  2. 典型应用场景:
按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

考察方向:

延伸递进面试题:

  1. volatile 能保证原子性吗?为什么?
  1. 有了 CAS 为啥还需要 volatile?
  1. DCL 单例为什么必须加 volatile?

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,volatile 是 Java 最经典的并发关键字,它保证两件事:可见性和有序性,但不保证原子性。

先说可见性。现代 CPU 每个核都有自己的 L1/L2 缓存,线程改了共享变量可能只改了自己核的缓存,别的核看不到。volatile 通过两层机制解决:

第一层是 CPU 层的 MESI 协议。Java 在写 volatile 变量时会生成 lock 前缀的指令,这条指令会强制把本核 L1 的 modified 数据写回主存,同时广播"无效"命令给其他核,让它们下次读必须从主存重新加载。

第二层是 JVM/编译器层。volatile 写会禁止 store-store 重排、volatile 读会禁止 load-load 和 load-store 重排。

再说有序性。JVM 层面定义了 4 种内存屏障:StoreStore、StoreLoad、LoadLoad、LoadStore。volatile 写之前是 StoreStore、写之后是 StoreLoad(也是最贵的屏障),volatile 读之后是 LoadLoad 和 LoadStore。这些屏障本质上是禁止 JIT 优化器把指令重排到破坏语义的位置。

关键的反例是 i++ 不安全。i++ 其实是三步:读 i、i+1、写回。volatile 只能保证每步的可见性,没法保证三步作为一个整体互斥,两个线程并发执行还是会丢更新。所以要用 AtomicInteger,或者加锁。

典型应用场景有几个:

第一个是状态标志位,比如 volatile boolean stop;,主线程改了从线程能立刻看到。

第二个是双重检查锁单例,private volatile static Instance instance;。这里必须加 volatile,因为 new 指令会被 JVM 重排成"分配内存→赋引用→初始化构造器",不加 volatile 时,另一个线程可能看到"已经赋引用但还没初始化"的半构造实例,直接用就 NPE。

第三个是双端队列的 tail 引用,比如 JDK 里 ArrayBlockingQueue 的 last 字段也是 volatile。

总结:volatile 用 MESI + 内存屏障保证可见性和有序性,但不保证原子性,所以适合单变量读写场景,复杂复合操作还是要用 synchronized 或 CAS。