volatile关键字的基本概念
一、问题背景:CPU 缓存带来的可见性问题
现代 CPU 为了提升性能,每个核心都有自己的高速缓存(Cache)。线程运行在不同核心上时,读取变量可能读的是各自缓存里的副本,而不是主内存中的最新值。
1.1 没有 volatile 时的代码
java
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 的缓存未失效
└──────────────┘
每个线程都有自己的工作内存:
java
主内存
flag=false
|
-------------------
| |
线程A缓存 线程B缓存
flag=false flag=true
线程 A 修改:flag=false,可能只是修改了自己的缓存,没有及时刷新到主内存。
核心问题: 线程 A 修改了主内存的值,但线程 B 的缓存副本不会自动失效,导致线程 B 看不到最新值。
这就是 可见性(Visibility)问题。
二、volatile 的诞生:解决可见性
Java 设计者意识到:程序员需要一个关键字,告诉 JVM "这个变量的每次读写都必须直接操作主内存,不要依赖缓存"。
于是 volatile 被创建出来。
2.1 加上 volatile 后
java
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 时的重排序问题
java
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 可能把 ① 和 ② 重排序:
java
// 实际执行顺序可能变成:
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 不能保证原子性
java
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 通常用于一个变量被多个线程共享,并且一个线程修改后,其他线程需要立即感知的场景。它保证变量的可见性和有序性,但不保证复合操作的原子性,所以不能替代锁
java
// ✅ 适合用 volatile 的场景:
// 1. 状态标志位(一个线程写,其他线程读)
private volatile boolean isRunning = true;
// 2. 双重检查锁(DCL)中的单例
private volatile static Singleton instance;
// 3. 读多写少,且写入不依赖当前值
private volatile long configVersion;
1、场景一:状态标志位(最常见)
java
public class Worker {
private volatile boolean running = true;
public void work(){
while(running){
// 执行业务
}
System.out.println("线程结束");
}
public void stop(){
running=false;
}
}
2、场景二:单例模式双重检查锁
java
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?
重点:
java
instance=new Singleton();
不是一步完成。
实际上:
第一步:分配对象内存:
java
memory = new Object()
第二步:初始化对象:
java
memory.name="xxx"
第三步:让 instance 指向对象:
java
instance = memory
但是 JVM 可能发生指令重排序,变成:
1. 分配内存
3. instance 指向内存
2. 初始化对象
于是:线程 A:
java
instance != null
以为对象创建好了。
但是:对象还没有初始化完成。导致空指针。
volatile 禁止这种重排序。
3、场景三:配置刷新
java
public class Config {
private volatile String address;
public void update(String newAddress){
address=newAddress;
}
public String getAddress(){
return address;
}
}
多个线程读取配置:
java
线程1 修改地址
线程2、3、4立即看到新地址
适合 volatile。
5-2、什么时候不用 volatile?
java
// ❌ 不适合的场景:
// 1. 需要原子性操作的计数器
private volatile int counter; // 错误!count++ 非原子
// 2. 多个线程同时修改同一个变量
// 应该用 AtomicInteger 或 synchronized
5-3、volatile 和 synchronized 区别
| volatile | synchronized | |
|---|---|---|
| 可见性 | ✅ | ✅ |
| 原子性 | ❌ | ✅ |
| 有序性 | ✅ | ✅ |
| 加锁 | ❌ | ✅ |
| 性能 | 高 | 相对低 |
volatile:
我只是告诉 JVM,这个变量变化后大家马上看到。
synchronized:
我不仅保证大家看到,还保证同一时间只有一个线程修改。
六、一句话记住 volatile
volatile告诉 JVM:这个变量是"共享的、易变的",每次读取都要去主内存拿最新值,每次写入都要立刻刷回主内存,并且不要打乱它周围的指令顺序。
但它不保证复合操作的原子性 ------那是 synchronized 和 java.util.concurrent.atomic 包该做的事。