Skip to content

volatile

并发世界里,volatile 是最轻量的同步机制。

但很多人用它用错了——以为加了 volatile 就线程安全了,结果踩了坑还不知道。

volatile 能做什么

volatile 做两件事:

  1. 保证可见性:写操作立即刷新到主内存,读操作立即从主内存读取
  2. 防止指令重排序:阻止编译器和 CPU 的优化重排

但它不保证原子性——i++ 这种复合操作,就算加了 volatile 也不行。

┌─────────────────────────────────────────────┐
│             volatile 的三个作用               │
├─────────────────────────────────────────────┤
│                                             │
│  1. 可见性:                                 │
│     写 → 立刻刷主内存                         │
│     读 → 立刻读主内存                         │
│                                             │
│  2. 有序性:                                 │
│     防止指令重排序                            │
│                                             │
│  3. 不保证原子性:                            │
│     i++ 仍是三步操作,volatile 保护不了        │
│                                             │
└─────────────────────────────────────────────┘

基本用法

状态标志位(volatile 的经典场景)

java
public class ShutdownFlagDemo {

    private volatile boolean running = true;

    public void run() {
        Thread worker = new Thread(() -> {
            while (running) {
                try {
                    System.out.println("工作中...");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
            System.out.println("线程停止");
        });

        worker.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        running = false;  // 写入后立即被 worker 线程看到
        System.out.println("请求停止");
    }

    public static void main(String[] args) throws InterruptedException {
        new ShutdownFlagDemo().run();
    }
}

没有 volatile,主线程设置 running=false 后,worker 线程可能永远看不到(因为 JIT 优化成了 while(true))。

单次初始化模式

java
public class VolatileInitDemo {

    private volatile boolean initialized = false;
    private volatile String resource;

    public void init() {
        if (!initialized) {
            synchronized (this) {
                if (!initialized) {
                    resource = loadResource();
                    initialized = true;
                }
            }
        }
    }

    public void use() {
        if (initialized) {
            System.out.println("使用资源: " + resource);
        }
    }

    private String loadResource() {
        return "已加载的资源";
    }

    public static void main(String[] args) {
        VolatileInitDemo demo = new VolatileInitDemo();
        demo.init();
        demo.use();
    }
}

这里 volatile 的作用:防止 initializedresource 的赋值顺序被重排,确保其他线程看到的是完整初始化后的状态。

观察 volatile 的效果

java
public class VolatileEffectDemo {

    // 普通变量
    private static boolean normalFlag = false;

    // volatile 变量
    private static volatile boolean volFlag = false;

    public static void main(String[] args) throws InterruptedException {
        // 测试非 volatile
        System.out.println("=== 普通变量测试 ===");
        testFlag("normalFlag", () -> normalFlag = true,
            () -> { while (!normalFlag) { Thread.yield(); } });

        // 测试 volatile
        System.out.println("\n=== volatile 变量测试 ===");
        testFlag("volFlag", () -> volFlag = true,
            () -> { while (!volFlag) { Thread.yield(); } });
    }

    private static void testFlag(String name, Runnable setter, Runnable waiter) {
        long start = System.nanoTime();
        Thread writer = new Thread(setter);
        Thread reader = new Thread(waiter);

        reader.start();
        writer.start();

        writer.join();
        reader.join();

        long elapsed = (System.nanoTime() - start) / 1_000_000;
        System.out.println(name + " 可见延迟: " + elapsed + "ms");
    }
}

在多核环境下,普通变量可能出现明显的可见延迟。

Double-Checked Locking:volatile 的经典应用

这是面试超高频题,单例模式中的 volatile:

java
public class Singleton {

    // 如果没有 volatile,以下问题可能发生:
    // 1. 分配内存
    // 2. 调用构造函数(创建对象)
    // 3. 将引用赋值给 instance
    // CPU 可能重排序为 1 -> 3 -> 2
    // 其他线程在第2步完成前就看到了非null的instance
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {                    // 第一次检查
            synchronized (Singleton.class) {        // 同步
                if (instance == null) {            // 第二次检查
                    instance = new Singleton();    // 创建
                }
            }
        }
        return instance;
    }

    private Singleton() {}
}

为什么需要两次检查

  • 第一次检查(if):避免不必要的同步。如果实例已经创建,直接返回,不需要抢锁
  • 第二次检查(if):防止多线程同时通过第一次检查,都去创建实例

为什么需要 volatile

防止 new Singleton() 的指令重排序。如果 2 和 3 重排:

线程A: instance = new Singleton(); // 1.分配内存 3.赋值引用(instance!=null)
                                        // 2.构造函数还没执行

线程B: if (instance == null) // false,跳过
       instance.doSomething(); // ❌ 访问未初始化的对象!

volatile 防止重排序,确保 1 -> 2 -> 3 的顺序。

计数器:volatile 的禁区

java
public class VolatileCounter {

    // ❌ 错误:volatile 不保证原子性
    private static volatile int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        int threadCount = 100;

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter++; // 非原子操作!volatile 只保证读写,不保证复合操作
                }
            }).start();
        }

        Thread.sleep(2000);
        System.out.println("期望: " + (threadCount * 1000));
        System.out.println("实际: " + counter);  // 小于期望值!
    }
}

这里 counter++ 的三个步骤(读、改、写)不是原子的,volatile 只能保证每次读取和写入是最新的,但两个线程同时 ++ 时仍然会丢失更新。

正确做法:AtomicInteger

java
// ✅ 正确
private static final AtomicInteger counter = new AtomicInteger(0);

// 使用
counter.incrementAndGet();  // 原子操作

volatile vs synchronized vs Atomic*

维度volatilesynchronizedAtomicInteger
可见性
原子性
防止重排序
阻塞否(CAS 自旋)
性能最高

适用场景

volatile 适合的场景

  • 只有一个线程写、多个线程读的状态标志
  • 单例模式中的对象引用
  • 配合 happens-before 规则传递状态

不适合的场景

  • 计数器(用 AtomicInteger)
  • 需要复合操作(用 synchronized 或 Atomic*)
  • 需要原子性保证的操作

注意事项

  1. volatile 不能替代 synchronized:只保证可见性和有序性,不保证原子性
  2. 对变量的写入不依赖当前值:如 flag = true 可以,counter++ 不行
  3. 没有死循环风险:如果写入依赖当前值,可能出现可见性问题
  4. 64位变量:JDK 5+ 的 volatile 保证 long/double 读写的原子性

要点回顾

  • volatile 保证可见性和有序性,不保证原子性
  • 适用于:状态标志、单例模式、配合 happens-before 传递状态
  • 不适用于:计数器、复合操作
  • 单例模式中 volatile 防止指令重排序导致访问未初始化对象

相关链接

基于 VitePress 构建