volatile 关键字
volatile关键字的基本概念
一、问题背景:CPU 缓存带来的可见性问题
现代 CPU 为了提升性能,每个核心都有自己的高速缓存(Cache)。线程运行在不同核心上时,读取变量可能读的是各自缓存里的副本,而不是主内存中的最新值。
1.1 没有 volatile 时的代码
public class VisibilityProblem { // 普通变量,无 volatile private boolean running = true; public void stop() { running = false; // 线程 A 修改 } public void doWork() { while (running) { // 线程 B 读取 // 执行任务... } System.out.println("已停止"); } }问题场景:
线程 A 调用
stop(),把running改为false线程 B 之前读取的是 running = ture, 导致
while (running)循环可能永远停不下来
1.2 为什么会这样?
┌─────────────┐ ┌─────────────┐ │ 线程 A │ │ 线程 B │ │ (核心 1) │ │ (核心 2) │ └──────┬──────┘ └──────┬──────┘ │ │ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ 缓存副本 A │ │ 缓存副本 B │ │ running=false │ │ running=true │ ← 线程 B 还在读旧值! └──────┬───────┘ └──────┬───────┘ │ │ └───────────┬───────────┘ ▼ ┌──────────────┐ │ 主内存 │ │ running=false│ ← 线程 A 已写入,但线程 B 的缓存未失效 └──────────────┘每个线程都有自己的工作内存:
主内存 flag=false | ------------------- | | 线程A缓存 线程B缓存 flag=false flag=true线程 A 修改:flag=false,可能只是修改了自己的缓存,没有及时刷新到主内存。
核心问题:线程 A 修改了主内存的值,但线程 B 的缓存副本不会自动失效,导致线程 B看不到最新值。
这就是可见性(Visibility)问题。
二、volatile 的诞生:解决可见性
Java 设计者意识到:程序员需要一个关键字,告诉 JVM"这个变量的每次读写都必须直接操作主内存,不要依赖缓存"。
于是volatile被创建出来。
2.1 加上 volatile 后
public class VisibilityFixed { // 加上 volatile private volatile boolean running = true; public void stop() { running = false; // 线程 A 写入 → 直接刷回主内存 } public void doWork() { while (running) { // 线程 B 读取 → 直接从主内存重新加载 // 执行任务... } System.out.println("已停止"); } }效果:
线程 A 写入
running = false时,会立即刷新到主内存线程 B 读取
running时,会使本地缓存失效,重新从主内存读取
这样线程 B 就能立刻看到false,循环正常退出。
三、volatile 的第二个作用:禁止指令重排序
3.1 什么是指令重排序?
编译器和CPU为了提高执行效率,可能会调整代码的执行顺序。
在单线程下这没问题,但在多线程下可能引发灾难。
3.2 没有 volatile 时的重排序问题
public class ReorderingProblem { private int value = 0; private boolean ready = false; // 线程 A 执行 public void writer() { value = 42; // ① ready = true; // ② } // 线程 B 执行 public void reader() { if (ready) { // ③ System.out.println(value); // ④ } } }没有 volatile 时,编译器/CPU 可能把 ① 和 ② 重排序:
// 实际执行顺序可能变成: ready = true; // ② 先执行 value = 42; // ① 后执行后果:
线程 B 看到
ready == true,但value还是0程序输出
0,而不是预期的42
3.3 volatile 如何禁止重排序?
volatile会在读写操作前后插入内存屏障(Memory Barrier),强制保证执行顺序:
线程 A 的 writer(): value = 42; ──[StoreStore 屏障]── // 保证 value 的写入在 ready 之前 ready = true; // volatile 写 ──[StoreLoad 屏障]── // 保证此操作之前的写入对其他线程可见 线程 B 的 reader(): if (ready) { // volatile 读 ──[LoadLoad 屏障]── // 保证 ready 的读取在 value 之前 System.out.println(value); }内存屏障的本质:一道"栅栏",阻止指令越过它进行重排序。
四、volatile 不能做什么?(重要)
4.1 volatile 不能保证原子性
public class NotAtomic { private volatile int count = 0; // 线程 A 和线程 B 同时执行 public void increment() { count++; // 这不是原子操作! } }count++实际上分三步:
读取count 的值
加 1
写回count
即使加了volatile,这三步之间仍可能被其他线程打断:
时间点 线程 A 线程 B t1 读取 count=0 t2 读取 count=0 t3 计算 0+1=1 t4 计算 0+1=1 t5 写入 count=1 t6 写入 count=1 最终结果:count = 1(期望是 2)结论:volatile解决不了原子性问题,需要用synchronized或AtomicInteger。
五、总结:volatile 到底做了什么?
| 特性 | 是否保证 | 原理 |
|---|---|---|
| 可见性 | ✅ 保证 | 读写直接操作主内存,缓存失效机制 |
| 禁止重排序 | ✅ 保证 | 内存屏障阻止指令乱序 |
| 原子性 | ❌ 不保证 | i++这类复合操作仍需同步 |
5-1、什么时候用 volatile?
volatile 通常用于一个变量被多个线程共享,并且一个线程修改后,其他线程需要立即感知的场景。它保证变量的可见性和有序性,但不保证复合操作的原子性,所以不能替代锁
// ✅ 适合用 volatile 的场景: // 1. 状态标志位(一个线程写,其他线程读) private volatile boolean isRunning = true; // 2. 双重检查锁(DCL)中的单例 private volatile static Singleton instance; // 3. 读多写少,且写入不依赖当前值 private volatile long configVersion;1、场景一:状态标志位(最常见)
public class Worker { private volatile boolean running = true; public void work(){ while(running){ // 执行业务 } System.out.println("线程结束"); } public void stop(){ running=false; } }2、场景二:单例模式双重检查锁
public class Singleton { private volatile static Singleton instance; public static Singleton getInstance(){ // 第一重检查 if(instance==null){ synchronized(Singleton.class){ // 第二重检查 if(instance==null){ instance=new Singleton(); } } } return instance; } }为什么需要 volatile?
重点:
instance=new Singleton();不是一步完成。
实际上:
第一步:分配对象内存:
memory = new Object()第二步:初始化对象:
memory.name="xxx"第三步:让 instance 指向对象:
instance = memory但是 JVM 可能发生指令重排序,变成:
1. 分配内存 3. instance 指向内存 2. 初始化对象于是:线程 A:
instance != null以为对象创建好了。
但是:对象还没有初始化完成。导致空指针。
volatile 禁止这种重排序。
3、场景三:配置刷新
public class Config { private volatile String address; public void update(String newAddress){ address=newAddress; } public String getAddress(){ return address; } }多个线程读取配置:
线程1 修改地址 线程2、3、4立即看到新地址适合 volatile。
5-2、什么时候不用 volatile?
// ❌ 不适合的场景: // 1. 需要原子性操作的计数器 private volatile int counter; // 错误!count++ 非原子 // 2. 多个线程同时修改同一个变量 // 应该用 AtomicInteger 或 synchronized5-3、volatile 和 synchronized 区别
| volatile | synchronized | |
|---|---|---|
| 可见性 | ✅ | ✅ |
| 原子性 | ❌ | ✅ |
| 有序性 | ✅ | ✅ |
| 加锁 | ❌ | ✅ |
| 性能 | 高 | 相对低 |
volatile:
我只是告诉 JVM,这个变量变化后大家马上看到。
synchronized:
我不仅保证大家看到,还保证同一时间只有一个线程修改。
六、一句话记住 volatile
volatile告诉 JVM:这个变量是"共享的、易变的",每次读取都要去主内存拿最新值,每次写入都要立刻刷回主内存,并且不要打乱它周围的指令顺序。
但它不保证复合操作的原子性——那是synchronized和java.util.concurrent.atomic包该做的事。
