volatile 关键字详解

119 阅读3分钟

volatile 关键字详解

一、volatile 的作用

volatile 是 Java 中用于确保多线程环境下共享变量可见性有序性的关键字。 它通过以下两种机制实现:

  1. 禁止指令重排序(通过内存屏障)。
  2. 强制读写直接操作主内存(而非线程的工作内存)。

二、核心特性

1. 可见性(Visibility)

  • 问题:普通变量可能被线程缓存在工作内存中,导致其他线程无法及时看到修改。

  • volatile 的解决方案

    • 写操作:立即将工作内存的值刷新到主内存。
    • 读操作:每次直接从主内存读取最新值。

示例

public class VisibilityDemo {
    private volatile boolean flag = false;
​
    public void start() {
        new Thread(() -> {
            while (!flag) {} // 能及时感知 flag 变化
            System.out.println("Flag is true");
        }).start();
​
        new Thread(() -> {
            flag = true; // 修改后对其他线程可见
        }).start();
    }
}

2. 有序性(Ordering)

  • 问题:编译器或处理器可能对指令重排序,导致多线程执行结果不符合预期。
  • volatile 的解决方案: 通过插入内存屏障(Memory Barrier),禁止指令重排序。

内存屏障类型

屏障类型作用
StoreStore禁止上方普通写与下方 volatile 写重排序。
StoreLoad禁止 volatile 写与后续 volatile 读/写重排序(开销最大)。
LoadLoad禁止下方普通读与上方 volatile 读重排序。
LoadStore禁止下方普通写与上方 volatile 读重排序。

三、volatile 的典型使用场景

1. 状态标志位

用于控制线程启停的标记变量:

public class Worker implements Runnable {
    private volatile boolean running = true;
​
    public void stop() {
        running = false;
    }
​
    @Override
    public void run() {
        while (running) { /* 执行任务 */ }
    }
}

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

解决单例对象初始化时的指令重排序问题:

public class Singleton {
    private static volatile Singleton instance;
​
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 无 volatile 时可能返回未初始化对象
                }
            }
        }
        return instance;
    }
}
  • volatile 的问题new Singleton() 可能被重排序为“分配内存 → 返回引用 → 初始化对象”,导致其他线程获取未初始化的实例。

3. 一次性发布(One-Time Safe Publication)

保证对象初始化完成后才对其他线程可见:

public class ResourceHolder {
    private volatile Resource resource;
​
    public Resource getResource() {
        if (resource == null) {
            synchronized (this) {
                if (resource == null) {
                    resource = new Resource(); // 安全发布
                }
            }
        }
        return resource;
    }
}

四、volatile 的局限性

1. 不保证原子性

  • 问题volatile 无法保证复合操作(如 i++)的原子性。

  • 示例

    public class Counter {
        private volatile int count = 0;
    ​
        public void increment() {
            count++; // 非原子操作(实际为 read-modify-write)
        }
    }
    
    • 即使 countvolatile 的,多线程并发调用 increment() 仍会导致计数错误。

2. 替代方案

  • 原子类(如 AtomicInteger):通过 CAS 实现原子操作。
  • (如 synchronized):保护临界区。

五、volatile 的实现原理

1. 内存语义

  • 写操作

    1. 修改工作内存中的值。
    2. 强制刷新到主内存(插入 StoreStoreStoreLoad 屏障)。
  • 读操作

    1. 从主内存重新加载最新值。
    2. 插入 LoadLoadLoadStore 屏障。

2. 底层实现

  • JVM 层面:通过内存屏障指令(如 x86 的 mfence)实现。
  • 硬件层面:依赖 CPU 的缓存一致性协议(如 MESI)。

六、volatile vs synchronized

特性volatilesynchronized
原子性不保证保证
可见性保证保证
有序性部分保证(禁止重排序)完全保证
阻塞可能阻塞线程
适用场景状态标志、DCL 单例临界区保护、复合操作
性能开销较高(但 JVM 已优化)

七、总结

1. 使用场景

  • 需要多线程可见性(如状态标志)。
  • 需要禁止指令重排序(如 DCL 单例模式)。

2. 注意事项

  • 不适用于复合操作(如 i++)。
  • 不能替代锁或原子类。

3. 最佳实践

  • 简单状态控制:优先使用 volatile
  • 复合操作:使用锁或原子类。
  • 安全发布对象:结合 volatile 与 DCL 模式。

通过合理使用 volatile,可以在不引入锁的情况下解决多线程的可见性和有序性问题,但需明确其适用边界,避免误用导致并发 Bug。