题目

请介绍一下 Java 中的 volatile 关键字,它的作用是什么?能保证原子性吗?和 synchronized 有什么区别?

参考答案

前言

"请介绍一下 volatile 关键字"——这道题在面试中的出现频率,大概跟早高峰地铁的拥挤程度差不多。

很多同学一听到 volatile,脑子里蹦出的第一个词是"可见性",然后就卡壳了。再问"它能保证原子性吗?""它和 synchronized 有啥区别?"——直接进入"阿巴阿巴"模式。

别慌。读完这篇文章,你不仅能把这几个问题讲得条理清晰,还能顺便甩出一个面试官大概率没听过的小细节,让他在心里给你默默加分。

一、volatile 到底是什么?——从一个翻车现场说起

假设你和室友共用一张便利贴,上面写着一个数字。你天天盯着便利贴看,想等数字变成 2 就出门买菜。结果你的眼睛像装了缓存似的——便利贴上明明已经写了 2,你眼里还是旧数据 1,于是你饿了一整天。

这就是多线程编程中的可见性问题。JVM 为了提高性能,允许每个线程把共享变量的副本缓存到自己的"工作内存"里(比如 CPU 寄存器或缓存),改的是副本,读的也是副本。什么时候同步回主内存?——看心情。

volatile 就是来解决这个问题的。被 volatile 修饰的变量,JVM 会跟你保证两件事:

  1. 写操作完成后,立即刷回主内存(不等了,当场就写)
  2. 读操作每次都去主内存拿最新值(不读缓存副本了)

用代码演一下:

// 不加 volatile
class WithoutVolatile {
    private boolean flag = false;

    public void writer() {
        flag = true;  // 写进工作内存,什么时候同步到主内存?不确定
    }

    public void reader() {
        while (!flag) {  // 可能永远读不到最新值,死循环!
            // 线程:不是说好改了吗?我咋看不见?
        }
        System.out.println("终于看到了!");
    }
}
// 加上 volatile
class WithVolatile {
    private volatile boolean flag = false;

    public void writer() {
        flag = true;  // 写完立刻刷回主内存,一秒都不拖
    }

    public void reader() {
        while (!flag) {  // 每次读都去主内存拿,保证是最新的
        }
        System.out.println("看到了!去买菜!");
    }
}

你可以这样记:不加 volatile,线程就像戴着墨镜看世界,看到的永远是旧的。加了 volatile,就是摘掉墨镜,看到什么就是什么。


二、JMM(Java 内存模型)视角——硬核但不吓人

要彻底理解 volatile,绕不开 JMM。别被这三个字母吓到,我们用一个图搞定:

  • 普通变量:线程A的工作内存 → 主内存(不知道啥时候同步),线程B的工作内存 → 主内存(不知道啥时候去取)
  • volatile 变量:线程A写 → 立刻同步主内存;线程B读 → 每次都从主内存取

这个"立刻同步"的能力,就叫可见性(Visibility)


三、禁止指令重排序——volatile 的第二项绝活

除了可见性,volatile 还有一个更隐蔽的能力:禁止指令重排序

什么叫指令重排序?看这个例子:

// 你写的代码
int a = 1;      // 语句1
int b = 2;      // 语句2
flag = true;    // 语句3

JVM 和 CPU 为了跑得更快,可能会把你的代码顺序打乱执行——比如先执行语句3,再执行语句1和2。在单线程下这完全没问题(反正最终结果一样),但在多线程下就可能翻车:

// 经典的双重检查锁定(DCL)单例——有 Bug 的版本
public class Singleton {
    private static Singleton instance;  // 没加 volatile!

    private Singleton() {}

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

instance = new Singleton() 这一行代码,在 JVM 眼里其实是三步操作:

  1. 分配内存空间
  2. 调用构造函数初始化对象
  3. 将 instance 引用指向分配的内存地址

问题来了——步骤2和步骤3可能被重排序!也就是说,可能先执行3再执行2。如果线程A在步骤3执行完后(此时 instance 不为 null 但还没初始化完),线程B刚好执行到第一次 if (instance == null) 检查——它发现 instance 不为 null,直接返回了一个半成品对象,然后程序就崩了。

解法很简单,给 instance 加上 volatile

private static volatile Singleton instance;  // ✅ 禁止指令重排序

volatile 会在写操作前后插入内存屏障(Memory Barrier),相当于写了一堵墙——墙两边的指令,休想跨过墙重排序。

用个不太恰当但好记的比喻:volatile 就像高速上的隔离带,你可以在自己的车道里随便变道,但别想跨过隔离带去对面车道逆行。


四、最容易被问趴的地方——volatile 不能保证原子性

"volatile 能保证原子性吗?"

正确答案:不能。

这是面试官最爱挖的坑。很多人觉得 volatile 又能保证可见性、又能禁止重排序,那是不是就万事大吉了?那可不行。

看这个经典的反例:

public class Counter {
    private volatile int count = 0;

    public void increment() {
        count++;  // 你以为这是安全的自增?错!
    }
}

count++ 看起来是一步操作,实际上是三步:

  1. 读:从主内存读取 count 的当前值
  2. 改:在 CPU 里把值 +1
  3. 写:把新值写回主内存

volatile 只保证了第1步读到的是最新值第3步立刻写回,但在第1步和第3步之间,另一个线程可能已经读到了同样的旧值并且也执行了 +1。最终结果就是两次自增只加了一次。

这就是著名的非原子操作下的竞态条件

时间 线程A 线程B count 主内存
T1 读取 count=0 0
T2 读取 count=0 0
T3 count=0+1=1 1(A刷回)
T4 count=0+1=1 1(B也刷回,但应该是2!)

要解决这个问题,你需要真正的原子操作——synchronizedAtomicInteger 或者 Lock

// 方案一:synchronized
public synchronized void increment() { count++; }

// 方案二:AtomicInteger(底层用 CAS,无锁更高效)
private AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); }

一句话总结:volatile 管得了视线(可见性),管不了手速(原子性)。


五、volatile vs synchronized —— 终极对比

面试官问完 volatile,大概率会跟一句:"那它和 synchronized 有什么区别?"这是送分题,也是一个展示你理解深度的好机会。

对比维度 volatile synchronized
本质 轻量级同步机制 重量级锁机制
修饰对象 只能修饰变量 修饰方法、代码块
保证可见性 ✅ 是 ✅ 是
禁止重排序 ✅ 是(部分) ✅ 是
保证原子性 不能 ✅ 能
阻塞线程 ❌ 不阻塞 ✅ 会阻塞
性能开销 很低(无上下文切换) 较高(涉及锁竞争和线程调度)
适用场景 状态标志、DCL 单例 复合操作、临界区保护

用一句人话来记:

volatile 就像一个"喊话的人"——它能让你听到最新消息(可见性),但管不了你听到了之后怎么做(原子性)。synchronized 则像"交通警察"——不仅能让你看到最新路况,还管着你什么时候能走、走哪条道。


六、volatile 的典型使用场景

不是所有变量都应该加 volatile。下面的场景才是它发光发热的地方:

场景1:状态标志位
public class Worker implements Runnable {
    private volatile boolean running = true;  // ★ volatile

    public void run() {
        while (running) {
            // 干活中...
        }
        System.out.println("收到停工通知,下班!");
    }

    public void stop() {
        running = false;  // 主线程一改,Worker 线程立刻感知
    }
}

这种场景用 volatile 简直完美——只涉及单次读写,不存在竞态条件。

场景2:双重检查锁定(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:CAS 中的变量

AtomicIntegerAtomicReference 等原子类的内部 value 字段,就是用 volatile 修饰的:

// AtomicInteger 源码片段
private volatile int value;

CAS 保证了原子性(Compare and Swap),volatile 保证了每次 CAS 操作读到的是最新值。


七、常见面试追问

Q1:volatile 能保证线程安全吗?

不能一概而论。 volatile 只能保证可见性禁止指令重排序,不能保证原子性。如果操作本身是原子的(比如对 boolean 或 int 的简单赋值),那 volatile 就够了。如果涉及复合操作(如 i++),还需要加锁或用原子类。

Q2:所有变量加上 volatile 不就好了?为啥不默认加?

两个原因:

  1. 性能开销:volatile 的读写直接从主内存走,绕过了 CPU 缓存,比普通变量慢不少。所有变量都加 volatile,程序性能会大打折扣。
  2. 大部分场景不需要:单线程里根本用不着,多线程里很多时候有 synchronized 或 Lock 已经搞定了。

Q3:volatile 和 AtomicInteger 选哪个?

  • 只是简单的读写状态标记(比如开关)→ volatile
  • 需要 i++ 等复合操作 → AtomicInteger(CAS 实现,无锁高效)
  • 需要多步操作的原子性(比如先检查再操作)→ synchronized 或 Lock

Q4:volatile 变量的自增操作用 volatile 能保证正确吗?

不能,这就是前面第四节的翻车现场。i++ 是读-改-写三步,volatile 只保证每步读到最新值,但不能阻止别人在你读和写之间也读。正确姿势是用 AtomicInteger.incrementAndGet()


八、一句话总结

volatile 是 Java 最轻量的同步机制——保证可见性,禁止指令重排,但不保证原子性。线程安全的及格线是 volatile,满分还得靠锁或原子类。


面试口语回答模版

面试官:"请介绍一下 volatile 关键字。"

你可以这样答:

volatile 是 Java 提供的一种轻量级同步机制。它主要有两个作用:

第一,保证可见性。被 volatile 修饰的变量,一个线程修改后,其他线程能立即看到最新值。这是因为 volatile 要求写操作立刻刷回主内存,读操作每次都从主内存取。

第二,禁止指令重排序。JVM 和 CPU 为了优化可能会打乱指令执行顺序,volatile 通过内存屏障来防止这种重排序。最典型的应用场景就是双重检查锁定的单例模式,如果 instance 不加 volatile,可能因为指令重排导致别的线程拿到一个还没初始化完的对象。

但是,volatile 不能保证原子性。像 i++ 这种复合操作,volatile 是管不了的,需要用 synchronized 或者 AtomicInteger 来解决。

和 synchronized 相比,volatile 是更轻量的,不会引起线程阻塞和上下文切换,但它只能修饰变量,synchronized 可以修饰方法和代码块,还能保证原子性。

实际开发中,volatile 适合用在状态标志位DCL 单例等场景。

Logo

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

更多推荐