真实面经题目 · 原创解析
volatile 的作用和底层原理是什么?
volatile 是 Java 并发里用于解决共享变量可见性和一定程度有序性的轻量级机制。它保证对 volatile 变量的写入对后续读取可见,并通过 JMM 规定的 happens-before 关系约束指令重排;但它不保证 i++、check-then-act 这类复合操作的原子性。
真实面经题目 · 原创解析
volatile 是 Java 并发里用于解决共享变量可见性和一定程度有序性的轻量级机制。它保证对 volatile 变量的写入对后续读取可见,并通过 JMM 规定的 happens-before 关系约束指令重排;但它不保证 i++、check-then-act 这类复合操作的原子性。
volatile 的作用可以分三层回答。第一,它保证可见性:一个线程写 volatile 变量后,其他线程再次读这个变量时必须能看到最新写入结果。第二,它保证有序性:JMM 对 volatile 读写建立了 happens-before 规则,禁止编译器和处理器把关键读写重排到 volatile 边界之外。第三,它不保证复合操作原子性,因为像 i++ 实际包含读、改、写三个步骤,多个线程仍可能交错执行。底层上,volatile 写通常带有释放语义,volatile 读通常带有获取语义,JVM 会在合适位置插入内存屏障,配合 CPU 缓存一致性协议,让写入刷新并让其他核心的缓存副本失效或重新读取。实际使用中,它适合停止标志、配置开关、DCL 单例中的实例引用、安全发布不可变对象等;如果需要互斥和复合状态一致性,应使用 synchronized、Lock 或 Atomic 类。
volatile 不是锁,它解决的是共享变量在多线程之间的可见性和有序性问题,而不是临界区互斥问题。普通共享变量可能被线程读取到工作内存、寄存器或 CPU 缓存中,另一个线程修改后,当前线程未必马上感知;volatile 要求每次读都按 JMM 的 volatile 读规则获取值,每次写都按 volatile 写规则发布值。因此它适合表达一个独立变量的状态变化,但不适合保护多个变量之间的不变量。
可见性可以理解为一个线程写入 volatile 变量后,这个写入对其他线程后续读取同一个变量必须可见。JMM 层面规定 volatile 写会把该线程之前的普通写入一并发布出去,volatile 读会让当前线程看到发布前的结果。硬件层面并不是简单地每次都直接访问主内存,而是通过缓存一致性协议、写缓冲刷新、缓存行失效等机制协同实现。
volatile 的有序性来自编译器和处理器重排限制。普通变量读写在不改变单线程语义的前提下可能被重排,但 volatile 读写会形成边界:volatile 写之前的普通写不能被重排到 volatile 写之后,volatile 读之后的普通读写不能被重排到 volatile 读之前。这个特性常用来发布对象或状态,让另一个线程看到标志位变化时,也能看到标志位变化之前已经写好的数据。
JMM 明确规定:对一个 volatile 变量的写 happens-before 后续任意线程对同一个 volatile 变量的读。这个规则是 volatile 面试回答的核心,因为它把可见性和有序性统一到一个可推理模型里。线程 A 在写 volatile 标志前完成一批普通写入,线程 B 读到这个 volatile 标志的新值后,理论上也应看到 A 在写标志之前的普通写入结果。
JVM 实现 volatile 时会在字节码对应的机器指令附近插入内存屏障,限制编译器优化和 CPU 乱序执行。常见抽象包括 StoreStore、StoreLoad、LoadLoad、LoadStore。volatile 写前后需要确保之前的写不会跑到 volatile 写之后;volatile 读后需要确保后续普通读写不能跑到 volatile 读之前。其中 StoreLoad 通常是成本较高、语义较强的屏障,因为它要处理写后读顺序。
volatile 对单次读或单次写有可见性保障,但不能把多个步骤合成为一个不可分割的整体。典型例子是 count++:线程先读取 count,再计算加一,再写回 count,即使 count 被 volatile 修饰,两个线程仍可能读到相同旧值并分别写回相同新值,导致更新丢失。类似地,先判断再更新、多个字段同时维护一致性、余额扣减这类逻辑都不能只靠 volatile。
双重检查锁单例中实例引用必须使用 volatile,核心原因不是可见性这么简单,而是防止对象构造过程与引用赋值重排。对象创建大致包括分配内存、初始化对象、把引用赋给变量;如果引用赋值先于初始化被其他线程观察到,另一个线程可能拿到半初始化对象。volatile 修饰实例引用后,写入引用具有发布语义,读取引用具有获取语义。
volatile、synchronized 和 Atomic 类解决的问题不同。volatile 成本较低,适合单个变量状态发布,不提供互斥;synchronized 提供互斥、可见性和有序性,能保护临界区内多个变量的一致性,但可能涉及锁竞争;Atomic 类基于 CAS 等机制提供无锁原子更新,适合计数器、引用替换、状态机转换等单变量或可封装的原子操作。
count++ 是读、改、写三个步骤。volatile 能让每次读写对线程可见,但不能阻止两个线程同时读到相同旧值,再分别写回相同新值。要保证递增不丢失,需要 AtomicInteger、LongAdder 或锁。
对同一个 volatile 变量的写 happens-before 后续任意线程对该变量的读。这意味着写 volatile 之前的普通写入,会对读到该 volatile 新值的线程可见,从而形成跨线程发布语义。
对象创建可能被重排为先分配内存并赋引用,再完成初始化。没有 volatile 时,其他线程可能看到非空引用却拿到半初始化对象。volatile 能限制引用发布与初始化之间的重排,并保证读线程看到构造后的状态。
volatile 提供可见性和有序性,但不提供互斥,也不保证复合操作原子性。synchronized 同时提供互斥、可见性和有序性,可以保护临界区内多个变量的一致性,但锁竞争成本更高。
当变量需要被多个线程并发更新,例如计数、自增、状态 CAS 转换时,volatile int 不够,因为更新是复合操作。AtomicInteger 通过 CAS 提供原子读改写,更适合这类场景。