返回首页面试题

面试必备:volatile 关键字深度解析

2026年03月25日8 min read

面试必备:volatile 关键字深度解析

volatile 是什么?

volatile 是 Java 中的轻量级同步机制,用来修饰变量。

public class VolatileDemo {
    volatile boolean flag = false;  // volatile 修饰
    
    public void writer() {
        flag = true;  // 写
    }
    
    public void reader() {
        if (flag) {   // 读
            // 一定能读到最新值
        }
    }
}

两个核心特性

1. 可见性(Visibility)

问题场景:多线程下,一个线程修改了变量,其他线程看不到。

public class VisibilityProblem {
    private static boolean flag = false;
    
    public static void main(String[] args) throws InterruptedException {
        // 线程 A:不断检查 flag
        new Thread(() -> {
            while (!flag) { }  // 可能永远不会停下来
            System.out.println("线程 A 检测到 flag = true");
        }).start();
        
        // 线程 B:修改 flag
        new Thread(() -> {
            flag = true;
            System.out.println("线程 B 修改 flag = true");
        }).start();
    }
}

原因:每个线程有自己的工作内存(CPU 缓存),对变量的读写先到缓存,不立刻写回主内存。

线程 A 的工作内存    线程 B 的工作内存    主内存
┌─────────────┐    ┌─────────────┐    ┌─────────┐
│   flag=true │    │   flag=false│ ←─→ │ flag=  │
│   (缓存)    │    │   (缓存)    │      │ false  │
└─────────────┘    └─────────────┘      └─────────┘
                         ↑
                   线程 B 修改后,其他线程
                   可能看不到这个改动

volatile 保证:变量被修改后,立刻写回主内存,并通知其他线程的缓存失效

volatile boolean flag = false;  // 加 volatile 后,立即可见

2. 有序性(Ordering)

问题场景:编译器/CPU 可能对代码进行重排序。

// 线程 A
initialized = true;       // 1. 初始化完成
writeRef(obj, instance);  // 2. 写入引用

// 线程 B
if (initialized) {        // 3. 检查是否初始化
    use(obj);             // 4. 使用对象
}

问题:如果重排序后,writeRef 先执行,线程 B 可能在对象还没初始化完成时就使用了它。

volatile 禁止重排序

// volatile 写前后的指令不能重排
volatile boolean initialized = false;

initialized = true;       // 这行之前的指令不能排到它后面
writeRef(obj, instance);  // 这行之后的指令不能排到它前面

底层实现:内存屏障

什么是内存屏障?

CPU 用来禁止指令重排序强制刷新缓存的机制。

volatile 的内存屏障

volatile 写操作:
┌────────────────────────────────────────┐
│ StoreStore 屏障                         │
│ 对 volatile 变量的写入                 │
│ StoreLoad 屏障                          │
└────────────────────────────────────────┘

volatile 读操作:
┌────────────────────────────────────────┐
│ LoadLoad 屏障                          │
│ 读取 volatile 变量                     │
│ LoadStore 屏障                          │
└────────────────────────────────────────┘

效果

  • volatile 写之前:前面的指令必须在写之前执行完
  • volatile 写之后:后面的指令必须在写之后执行
  • volatile 读之前:后面的指令必须在读之前执行完
  • volatile 读之后:前面的指令必须在读之后执行

volatile vs synchronized

特性volatilesynchronized
作用对象变量方法/代码块
原子性不保证保证
可见性保证保证
有序性保证保证
性能轻量级重量级

volatile 不保证原子性

volatile int count = 0;

// 两个线程各执行 10000 次 ++
count++;  // ++ 操作不是原子的!

count++ 实际是三步操作:

  1. 读取 count
  2. count + 1
  3. 写回 count

两个线程可能同时读到相同的值,结果小于 20000。


volatile 的应用场景

1. 状态标志

volatile boolean running = true;

public void shutdown() {
    running = false;  // 通知线程停止
}

public void run() {
    while (running) {
        // 执行业务逻辑
    }
}

2. 双重检查锁定(单例模式)

public class Singleton {
    private static volatile Singleton instance;  // volatile 关键!
    
    public static Singleton getInstance() {
        if (instance == null) {                    // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {              // 第二次检查
                    instance = new Singleton();     // 创建对象
                }
            }
        }
        return instance;
    }
}

为什么要 volatile?

instance = new Singleton() 实际是三步:

  1. 分配内存
  2. 调用构造函数
  3. 赋值给 instance

没有 volatile,步骤 2 和 3 可能重排序,导致其他线程拿到未构造完成的对象。

3. DCL(Double Checked Locking)原理

线程 A                      线程 B
────────────────────────    ────────────────────────
instance = new Singleton()
  ↓
分配内存
  ↓                        if (instance == null)  // 可能是半初始化对象
instance = 地址A             return instance  ← 返回一个"看起来正常"的对象
  ↓                        use(instance)   ← 错误!对象还没构造完
构造对象

volatile 禁止了重排序,避免这个问题。


JMM(Java 内存模型)

volatile 的语义实际上是 JMM 的一部分:

                    JMM
        ┌───────────────────────────┐
        │                           │
        │   ┌─────────┐  ┌─────────┐ │
        │   │Thread A │  │Thread B │ │
        │   │工作内存 │  │工作内存 │ │
        │   └────┬────┘  └────┬────┘ │
        │        │            │       │
        │        └─────┬──────┘       │
        │              │              │
        │         ┌────▼────┐        │
        │         │  主内存   │        │
        │         └─────────┘        │
        │                           │
        └───────────────────────────┘
        
volatile 保证:
├─ 写 → 主内存(立即刷新)
└─ 读 → 从主内存读取(绕过缓存)

面试高频问题

Q1:volatile 是怎么保证可见性的?

通过写屏障(Store Barrier)读屏障(Load Barrier)

  • 写:强制将值写入主内存
  • 读:强制从主内存读取,并让其他线程的缓存失效

Q2:volatile 能保证原子性吗?

不能。volatile 只保证可见性和有序性,不保证复合操作(如 count++)的原子性。

Q3:volatile 和 synchronized 哪个性能好?

volatile 性能好(轻量级),但功能也更少。synchronized 功能强,但重量级。

Q4:volatile 修饰 long、double 有什么特殊?

  • volatile 修饰 long/double:保证 64 位操作的原子性
  • JVM 中,对 non-volatile long/double 的读写可能分成两个 32 位操作
  • volatile 修饰后,保证读写是原子的

总结

volatile 的两个特性:

1. 可见性
   └─ 修改后立即写主内存
   └─ 读取时强制从主内存读
   └─ 通知其他 CPU 缓存失效

2. 有序性
   └─ 禁止指令重排序
   └─ 依赖内存屏障实现

使用场景:
├─ 状态标志(boolean flag)
├─ 一次性安全发布(单例模式)
└─ 读取远多于写入的场景

注意:
├─ volatile 不保证原子性
├─ synchronized 保证原子性
└─ volatile 是轻量级,synchronized 是重量级

评论区