volatile 这个关键字平时零零散散接触过——单例双重检查锁里见过、标志位也写过,但"它到底保证了什么、为什么不能保证原子性"一直没系统搞明白。这篇把 volatile 讲透:它的作用、背后的 Java 内存模型、以及和 synchronized 的配合。
一、volatile 的作用(核心)
volatile 就干两件事,还有一件干不了:
① 保证可见性:一个线程改了 volatile 变量,其他线程立刻能看到最新值。
volatile boolean running = true; // 加了 volatile
// 线程 A:running = false; → 立刻刷回主内存
// 线程 B:while (running) → 每次都从主内存读,能看到 false,退出循环② 禁止指令重排(有序性):JVM 为了性能会重排指令,volatile 通过"内存屏障"禁止它周围的重排。
❌ 不保证原子性:count++ 这种"读-改-写"三步操作,volatile 管不住中间被打断——两步之间别的线程也能进来。
volatile int count = 0;
count++; // ❌ 三步操作,volatile 保证不了原子性,多线程下仍会丢数据一句话记:volatile 管"看得见"(可见性)和"顺序对"(有序性),不管"不会被拆"(原子性)。
二、为什么会有"看不见":Java 内存模型
可见性问题来自 Java 内存模型(JMM):每个线程有自己的工作内存(缓存),共享变量存在主内存里:
线程读写变量,其实是先操作自己的工作内存副本,再同步到主内存。所以线程 A 改了 running,只是改了 A 的工作内存,可能还没刷回主内存;线程 B 读的还是自己工作内存里的旧值——两个线程各看各的副本,就"看不见"对方的修改。volatile 强制读写都经过主内存,解决了这个问题。
三、volatile vs synchronized(一张表记住)
| 维度 | volatile | synchronized |
|---|---|---|
| 保证 | 可见性 + 有序性 | 原子性 + 可见性 + 有序性 |
| 原子性 | ❌ 不保证 | ✅ 保证 |
| 是否阻塞 | 否(无锁) | 是(加锁,可能阻塞) |
| 性能 | 高 | 较低 |
| 适用 | 标志位、单次读写 | 复合操作、临界区 |
选型:只是"一个线程写、其他线程读"的标志位,用 volatile(轻量、无锁);要保证"读-改-写"的原子性,用 synchronized 或原子类(AtomicInteger)。
四、典型应用场景
① 状态标志位(最常见):
volatile boolean running = true;
public void stop() { running = false; } // 停止线程
public void run() { while (running) { ... } }② 单例双重检查锁(DCL):靠 volatile 禁止"引用先指向、初始化延后"的重排,保证拿到的一定是完整对象(见《单例模式》的双重检查锁那节)。
小结
volatile管两件事:可见性(读写经过主内存)+ 有序性(禁止指令重排)- 不保证原子性——
count++这类复合操作管不住 - 标志位用
volatile(轻量),复合操作用synchronized/原子类
想了解 volatile 在并发里的完整定位(和 synchronized、CAS 一起怎么配合),看《Java 并发编程》;想了解可见性背后的 JMM 主内存/工作内存模型,看《JVM 内存模型与垃圾回收》。
