volatile——面试中你有遇到过吗?

你好,我是吴计可师,一个工作十多年的后端开发,曾就职京东、阿里等多家互联网头部企业。

点击下方👇关注公众号,带你一起复习后端技术,看看面试考点,补充积累技术知识,每天都为面试准备积累

文章可能会比较长,主要解析的非常详解,或涉及一些底层知识,供面试高阶难度用。可以根据自己实际理解情况合理取舍阅读


volatile 是 Java 中的一个关键字,用来修饰变量,保证线程间的可见性一定的有序性


01
volatile 的作用


可见性:一个线程修改了 volatile 变量的值,新值对其他线程立即可见。

禁止指令重排序:对 volatile 变量的读写操作不能被重排序。

不保证原子性:volatile 仅保证读写的可见性,不保证复合操作(如 i++)的原子性。


02
实现原理


内存可见性原理

volatile 的实现主要依赖于 CPU 的 缓存一致性协议(MESI) 和 Java 内存模型(JMM)的实现。其核心机制如下:

  • 当一个线程对 volatile 变量进行写操作时,CPU 会将该变量所在的缓存行标记为无效,从而强制其他线程从主内存重新读取变量值。

  • 内存屏障:编译器会在 volatile 变量的读写操作前后插入特定的指令,用来限制指令重排序并实现内存可见性。

内存屏障

Java 编译器会在 volatile 变量的操作前后插入以下两种内存屏障:

Store屏障(写屏障)用于确保在写入 volatile 变量之前,所有的普通写操作都已经同步到主内存中。

Load屏障(读屏障)用于确保在读取 volatile 变量之后,所有的普通读操作都从主内存中获取最新值。

volatile int a = 1;a = 2;

编译器会生成:

  • 写屏障:确保之前的写操作完成后再更新 a 的值。

  • 将 a = 2 的值刷新到主内存。




读操作示例:
int b = a;

编译器会生成:读屏障:确保在读取 a 之前重新从主内存中加载。


CPU 指令的支持

在底层,volatile 的实现通常通过以下机制:

使用 LOCK 指令(在 x86 架构中)。这会:
  • 强制当前 CPU 缓存行写入主内存。

  • 使其他 CPU 的缓存行无效,确保数据一致性。

lock addl $0x0, (%esp)

此指令会强制执行内存屏障,防止指令重排序,并刷新主存数据。


03
volatile 和原子性


volatile 并不保证操作的原子性。例如:

volatile int count = 0;public void increment() {    count++; // 不是线程安全的}

count++ 实际上由三步组成:

  • 读取 count 的值。

  • 对值加 1。

  • 将结果写回到 count。

由于 volatile 不保证这三步的原子性,可能会出现多个线程竞争导致最终结果不正确。为此,需要使用原子类或同步机制(如 AtomicInteger 或 synchronized)。


04
使用场景


状态标志:

volatile boolean stop = false;

多线程通过共享变量控制任务的停止。

单例模式的双重检查锁(DCL):

public class Singleton {    private static volatile Singleton instance;    public static Singleton getInstance() {        if (instance == null) {            synchronized (Singleton.class) {                if (instance == null) {                    instance = new Singleton();                }            }        }        return instance;    }}

volatile 确保实例的初始化过程对所有线程可见,并防止指令重排序。


05
直击面试


5.1 volatile 和 synchronized 的区别是什么?

特性volatilesynchronized
功能保证变量的可见性、有序性保证代码块的原子性和可见性
性能较轻量,开销低比较重,涉及线程的上下文切换
适用场景单变量的读写多线程协作的复杂场景
阻塞非阻塞阻塞


5.2 为什么 volatile 不能代替 synchronized?

volatile 不能替代 synchronized 的主要原因:

  • volatile 不保证复合操作(如 i++)的原子性。

  • volatile 无法同步复杂逻辑(如代码块中的多变量操作)。

  • volatile 仅用于标识简单的状态标志或变量共享,而 synchronized 可以保护一段代码块的线程安全。


5.3 单例模式中为什么要用 volatile?

在单例模式的双重检查锁(DCL)中,需要用 volatile 修饰单例实例变量,避免因指令重排序导致其他线程访问到尚未完全初始化的实例。

public class Singleton {    private static volatile Singleton instance;    public static Singleton getInstance() {        if (instance == null) {            synchronized (Singleton.class) {                if (instance == null) {                    instance = new Singleton(); // 非原子操作                }            }        }        return instance;    }}

实例化对象的过程:

  • 分配内存。

  • 初始化对象。

  • 将内存地址赋值给变量。

由于指令重排序,可能导致第 3 步在第 2 步之前完成,从而使其他线程访问到尚未初始化的对象。


5.4 如何实现一个停止线程的安全方式?

使用 volatile 变量作为标志位,保证线程之间的可见性:

class StopThread implements Runnable {    private volatile boolean stop = false;    public void run() {        while (!stop) {            // 执行业务逻辑        }    }    public void stop() {        stop = true;    }}

volatile 确保 stop 的修改对其他线程可见,安全地终止线程。


5.5 使用 volatile 的最佳场景是什么?

  • 状态标志:如停止线程的标志。

  • 单例模式的双重检查锁。

  • 轻量级的读多写少场景。


5.6 什么是 volatile 的内存语义?

写 volatile 变量时:

  • 线程会将该变量值刷新到主内存。

  • 之前的写操作对其他线程可见。

读 volatile 变量时:

  • 线程会从主内存读取最新值。

  • 后续的读操作能够看到最新值。

内存屏障在底层通过 lock 指令 实现,防止 CPU 指令重排序。


5.7 如何验证 volatile 是否禁止指令重排序?

通过代码示例验证指令重排序是否被禁止:

class Test {    private volatile boolean flag = false;    private int value = 0;    public void writer() {        value = 42;    // 1        flag = true;   // 2    }    public void reader() {        if (flag) {    // 3            System.out.println(value); // 一定是 42        }    }}

因为 flag 是 volatile 修饰,语义上保证 1 的操作一定先于 2。在 reader 中如果看到 flag 为 true,则一定能读取到最新的 value。


今天的内容就分享到这儿,喜欢的朋友可以关注,点赞。有什么不足的地方欢迎留言指出,您的关注是我前进的动力!

END


扫码关注

一起积累后端知识
不积跬步,无以至千里
不积小流,无以成江海

喜欢此内容的人还喜欢

谈谈id那些事(五)——美团的 Leaf 的ID生成


一个阿里二面面试官必问的问题


谈谈id那些事(三)——阿里巴巴的 TDDL的ID生成


分享面试:mysql数据库索引失效的情况


面试常被忽略的问题——内存区域划分