基于本文回答
0
评论

Java中的volatile关键字

知识点图片

在 Java 中,volatile 是一个轻量级的同步机制,主要用于处理多线程并发中的可见性有序性问题,但它不保证原子性

要彻底理解 volatile,我们需要从它解决的三个核心并发特性入手:可见性、有序性和原子性。


一、 volatile 的三大特性

1. 保证“可见性”(Visibility)

什么是可见性?
在 Java 内存模型(JMM)中,所有变量都存储在“主内存”中,每个线程都有自己的“工作内存”(本地缓存)。线程读写变量时,会先从主内存拷贝到工作内存,修改后再写回主内存。这就导致一个线程修改了变量,另一个线程可能还在使用自己工作内存中的旧值。

volatile 的作用:
当一个变量被声明为 volatile 时,它会保证:

  • 写操作:一旦某个线程修改了该变量的值,新值会立即被强制刷新到主内存中。
  • 读操作:任何线程读取该变量时,都会强制使自己工作内存中的缓存失效,直接去主内存中读取最新值。

代码示例(可见性问题):

java
public class VolatileDemo {
    // 如果不加 volatile,主线程对 flag 的修改,子线程可能永远看不到,导致死循环
    private static volatile boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.println("子线程启动,等待 flag 变为 true...");
            while (!flag) {
                // 如果没有 volatile,这里会陷入死循环
            }
            System.out.println("子线程检测到 flag 变为 true,结束!");
        }).start();

        Thread.sleep(1000); // 主线程睡1秒
        flag = true; // 主线程修改 flag
        System.out.println("主线程已将 flag 改为 true");
    }
}

2. 禁止“指令重排序”保证“有序性”(Ordering)

什么是指令重排序?
编译器和 CPU 为了提高执行效率,在不改变单线程执行结果的前提下,可能会打乱代码的执行顺序。但在多线程环境下,这会导致严重的问题。

volatile 的作用:
volatile 底层通过插入内存屏障(Memory Barrier)来禁止特定类型的指令重排序。

  • volatile 变量时:在写指令前插入 StoreStore 屏障,写指令后插入 StoreLoad 屏障。
  • volatile 变量时:在读指令后插入 LoadLoad 和 LoadStore 屏障。
    简单来说,它确保了在 volatile 变量写操作之前的代码,绝对不会被重排序到写操作之后。

3. 不保证“原子性”(Atomicity)

什么是原子性?
一个操作不能被中断,要么全部执行完毕,要么完全不执行。

volatile 的局限性:
volatile 无法保证复合操作的原子性。例如 i++
i++ 实际上包含了三个步骤:

  1. 读取 i 的值到寄存器。
  2. i 加 1。
  3. 将新值写回内存。

如果两个线程同时执行 i++,即使 ivolatile 的,它们也可能同时读到相同的旧值,各自加 1 后写回,导致结果少加。

代码示例(非原子性):

java
public class VolatileAtomicityDemo {
    private static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                count++; // 非原子操作
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        // 预期结果是 2000,但实际结果通常小于 2000
        System.out.println("最终 count 值: " + count); 
    }
}

解决非原子性: 使用 synchronizedLock,或者使用 java.util.concurrent.atomic 包下的原子类(如 AtomicInteger)。


二、 volatile 的底层原理

volatile 的底层实现依赖于硬件级别的支持(如 x86 架构下的 lock 前缀指令)。
当对 volatile 变量进行写操作时,CPU 会执行带有 lock 前缀的指令,这会引发两件事:

  1. 将当前处理器缓存行的数据写回系统内存。
  2. 这个写回内存的操作会使得在其他 CPU 里缓存了该内存地址的数据无效(依靠缓存一致性协议,如 MESI 协议)。

三、 volatile 的经典应用场景

1. 状态标志位(Status Flag)

用于作为一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。

java
volatile boolean shutdownRequested = false;

public void shutdown() {
    shutdownRequested = true;
}

public void doWork() {
    while (!shutdownRequested) {
        // 执行业务逻辑
    }
}

2. 双重检查锁定(Double-Checked Locking,DCL)实现单例模式

这是 volatile 禁止指令重排序最经典的例子。

java
public class Singleton {
    // 必须加 volatile
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查
                    // instance = new Singleton() 实际上由三步组成:
                    // 1. 分配内存空间
                    // 2. 初始化对象
                    // 3. 将 instance 引用指向分配的内存地址
                    // 如果没有 volatile,可能会发生指令重排序变成 1 -> 3 -> 2
                    // 此时另一个线程执行到第一次检查,发现 instance != null,直接返回,
                    // 但此时对象还没初始化完成,导致使用时报错!
                    instance = new Singleton(); 
                }
            }
        }
        return instance;
    }
}

四、 volatilesynchronized 的对比

特性 volatile synchronized
性质 轻量级,修饰变量 重量级,修饰方法或代码块
是否阻塞 不会引起线程阻塞 可能会引起线程阻塞和上下文切换
可见性 保证 保证
有序性 保证(禁止指令重排序) 保证(通过单线程执行保证相对有序)
原子性 不保证 保证

五、 总结

  • 用一句话概括: volatile 告诉编译器和 CPU:“这个变量是多线程共享的,不要把它缓存到寄存器,每次都必须去主内存读写,并且不要打乱关于它的指令顺序。”
  • 适用前提: 对变量的写操作不依赖于当前值(例如 i++ 就不行,但 flag = true 可以),或者能确保只有单一线程修改变量的值。
右滑查看面试常问