Skip to content

volatile关键字

前置知识

在阅读本章前,你需要了解:

  • Java 内存模型(JMM)的基本概念
  • 多线程的基本知识
  • Java 中同步机制(synchronized)的初步理解

为什么需要 volatile?

我们先从一个现实问题说起吧:假设你和朋友一起玩传话游戏,你说一句话,朋友听到后再传给下一个人。现在,如果朋友在听的时候走神了,没把你的话听清楚,或者传的时候改了内容,整个信息链路就乱套了。

在多线程环境中,编程也是类似的:一个线程写入了变量的值,另一个线程需要及时看到这个新值,否则它可能“听到”的还是旧的信息,导致程序出现错乱。这里的“听到”就是变量的内存可见性问题。

volatile 关键字,就是用来保证变量更新后,所有线程都能马上看到最新值的“传话保证”。它还可以防止 JVM 为了性能优化,对代码执行顺序进行的“自由调整”(也叫指令重排序),这样让代码更“守规矩”。

不过,要知道,volatile 只能解决部分并发问题,用于修饰状态标志或者简单的读写。复杂原子操作还得靠锁(sync),咱们接下来慢慢拆解这背后的逻辑和使用技巧。


volatile到底是什么?

简单定义

volatile 是 Java 中的一个轻量级同步机制。声明一个变量为 volatile,保证:

  1. 内存可见性:线程对变量的修改对其他线程立即可见。
  2. 禁止指令重排序:防止编译器或处理器乱序执行对这个变量读写操作。

为什么需要它?

默认情况下,线程有自己的工作内存缓存,变量的读写不一定马上刷新到主内存,导致不同线程中的变量副本不一致,也就是“看不到最新值”。volatile 保证变量直接读写主内存,避免脏数据。

基础用法

java
public class VolatileExample {
    private volatile boolean flag = false;

    public void setFlagTrue() {
        flag = true;
    }

    public boolean getFlag() {
        return flag;
    }
}

这段代码里,任何一个线程调用 setFlagTrue(),写入的 flag 变化都会对调用 getFlag() 的所有线程马上可见。


代码示例一:简单的内存可见性演示

java
public class VisibilityDemo {
    private volatile boolean flag = false;

    public void changeFlag() {
        System.out.println("Thread1: 睡眠前 flag = " + flag);
        flag = true;
        System.out.println("Thread1: flag 已修改为 true");
    }

    public void readFlag() {
        while (!flag) {
            // 不断等待 flag 变为 true
        }
        System.out.println("Thread2: 读取到 flag = true,跳出循环");
    }

    public static void main(String[] args) {
        VisibilityDemo demo = new VisibilityDemo();

        new Thread(() -> demo.readFlag()).start();

        try {
            Thread.sleep(1000); // 保证线程2先进入循环
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> demo.changeFlag()).start();
    }
}

这段代码做了什么:

  1. 线程2不停读取 flag,直到它变为 true。
  2. 线程1 将 flag 设置为 true。
  3. 因为 flag 是 volatile 类型,线程2马上能看到修改,跳出循环。

内存可见性保障了共享变量的及时同步。


指令重排序与 volatile 的禁止效果

JVM 和 CPU 为了提升性能,会对代码执行顺序做调整,这叫做指令重排序。例如:

java
int a = 1;
int b = 2;
int c = a + b;

理论上按上面顺序执行,但实际上 CPU 可能先计算 c,然后赋值。大多数时候没问题,但在多线程操作共享变量时可能导致异常。

volatile 变量的访问会插入 内存屏障(Memory Barrier),保证:

  • volatile 写操作之前的代码不会被重排序到后面
  • volatile 读操作之后的代码不会被重排序到前面

这样串起线程间的执行顺序。


代码示例二:禁止指令重排序作用演示

下面,用一个经典例子展示 volatile 防止重排序导致的共享对象引用问题。

java
class Singleton {
    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 指向内存地址
                }
            }
        }
        return instance;
    }
}

这段代码做了什么:

  • 通过双重检查锁定(Double-Check Locking)实现线程安全的单例模式。
  • instance 加上 volatile 防止指令重排序导致半初始化对象被其他线程见到。

如果没有 volatile,instance 可能在对象没初始化完成前就被赋值,另一个线程拿到一个“半成品”对象,程序崩溃。


代码示例三:复杂场景——状态标志控制线程终止

java
import java.util.concurrent.TimeUnit;

public class VolatileStopFlag {
    private volatile boolean stopRequested = false;

    public void requestStop() {
        stopRequested = true;
    }

    public void doWork() {
        System.out.println("线程开始工作");
        int count = 0;
        while (!stopRequested) {
            count++;
            // 模拟工作
        }
        System.out.println("线程结束,计数=" + count);
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileStopFlag example = new VolatileStopFlag();

        Thread worker = new Thread(example::doWork);
        worker.start();

        TimeUnit.SECONDS.sleep(1);

        example.requestStop();
        worker.join();
        System.out.println("主线程结束");
    }
}

这段代码做了什么:

  • 线程持续执行 doWork() 方法里的循环,直到 stopRequested 变为 true。
  • 另一个线程调用 requestStop() 设置 stopRequested
  • stopRequested 声明为 volatile,保证工作线程马上能感知到停止信号,优雅退出循环。

对比总结

特性synchronizedvolatileAtomic 类
保证原子性支持不支持支持
保证可见性支持支持支持
禁止指令重排序
性能开销较大,涉及锁竞争较小,轻量级介于两者之间,有CAS操作开销
适用场景需要复杂同步及原子操作时简单的状态标志或可见性需求计数器等原子递增递减场景

volatile 不会为你加锁,不能保证复合操作的原子性(比如 count++),因此不能代替 synchronized 和 AtomicInteger 处理所有并发问题。


💡 实战建议

  • volatile 适合做“状态标志”和保证共享变量的可见性
  • 对于复合操作(读-改-写),还是用锁或者原子类。
  • 谨慎使用双重检查锁模式,务必把变量声明为 volatile。
  • 不推荐滥用 volatile,滥用会让代码难读且不可维护。
  • 若对性能敏感,先用 volatile 验证思路,然后确认有无竞争再加锁优化。

⚠️ 常见陷阱

  • 误用:认为 volatile 能保证原子性。
  • 漏加 volatile:双重检查单例中,漏了 volatile 导致不可预料的线程安全问题。
  • 死循环:没有加 volatile,循环中读取的变量始终是线程副本,导致无法跳出。
  • 过度使用:把 volatile 当万能钥匙,导致代码可读性和可维护性下降。

小结

  • volatile 关键字保证了变量的内存可见性和防止指令重排序。
  • 它适合修饰简单状态标志,保证所有线程及时看到最新值。
  • volatile 不能保证变量操作的原子性,需要针对原子操作用锁或原子变量。
  • 结合实际场景合理使用 volatile,是高效且安全并发编程的关键。
  • 了解 volatile 原理,能避免许多多线程编程的常见错误。

希望这章的讲解让你对 volatile 不再陌生。其实并发问题挺棘手,但掌握这些核心原理后,我们就掌握了看透多线程黑魔法的钥匙。后续遇到同步和并发难题,别忘了回头看看这部分哦。我们下章见!