三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

volatile 关键字

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++实际上分三步:

  1. 读取count 的值

  2. 加 1

  3. 写回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解决不了原子性问题,需要用synchronizedAtomicInteger


五、总结: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 或 synchronized

5-3、volatile 和 synchronized 区别

volatilesynchronized
可见性
原子性
有序性
加锁
性能相对低

volatile:

我只是告诉 JVM,这个变量变化后大家马上看到。

synchronized:

我不仅保证大家看到,还保证同一时间只有一个线程修改。


六、一句话记住 volatile

volatile告诉 JVM:这个变量是"共享的、易变的",每次读取都要去主内存拿最新值,每次写入都要立刻刷回主内存,并且不要打乱它周围的指令顺序。

但它不保证复合操作的原子性——那是synchronizedjava.util.concurrent.atomic包该做的事。

← 返回列表