目录
[二、为什么需要 volatile](#二、为什么需要 volatile)
[四、Volatile 不能做什么](#四、Volatile 不能做什么)
[七、JMM Happens-Before 规则](#七、JMM Happens-Before 规则)
[九、Volatile 与 Synchronized 核心对比](#九、Volatile 与 Synchronized 核心对比)
一、什么是volatile
volatile 是 Java 轻量级同步关键字,依靠内存屏障实现:保证可见性、禁止特定重排序、不保证原子
它只解决多线程并发的 可见性、重排序 问题,不解决 原子性、线程互斥 问题
volatile本质:
1、通过内存屏障禁止特定指令重排序
2、通过缓存一致性协议保证可见性
3、不保证原子性
4、适用于单写多读场景
5、DCL单例必须使用volatile
二、为什么需要 volatile
Java 并发编程中,多线程共享变量会出现三类核心问题,根源是 JMM 内存模型、CPU 缓存、指令优化:
1.1 可见性问题
每个线程拥有独立的工作内存(CPU 缓存),共享变量存储在主内存
普通变量:线程修改后仅更新本地缓存,不会立即刷入主内存,其他线程永远读取旧值
1.2 有序性问题(指令重排序)
编译器、CPU 为提升性能,会在单线程结果不变的前提下打乱代码执行顺序,多线程环境下会出现逻辑错乱
1.3 原子性问题
复合操作(读-改-写)无法一次性执行完毕,多线程下会出现覆盖更新、数据丢失。
volatile 就是为了解决这 3 个问题中的前两个:
可见性:一个线程修改了变量,其他线程能立刻看到最新值
有序性:代码执行顺序不被 CPU 优化打乱(禁止指令重排)
原子性:一个操作不可中断,要么全部执行,要么全部不执行
volatile 不解决原子性
三、Volatile能做什么
2.1 保证可见性
被 volatile 修饰的变量:
-
写操作:立即清空工作内存,强制刷新数据到主内存
-
读操作:清空本地缓存,强制从主内存加载最新数据
底层依赖 CPUMESI 缓存一致性协议:修改变量后,其他 CPU 的缓存行失效,必须重新从主内存读取
2.2 保证有序性(禁止特定指令重排序)
volatile 通过插入 内存屏障,禁止编译器和 CPU 对跨屏障指令做重排序,解决多线程重排序安全问题
四、Volatile 不能做什么
volatile 存在两大致命短板:
3.1 不保证原子性
仅能保证单次读写的原子性,复合操作完全不保证
典型案例:count++
底层分为三步:读取变量值 → 运算+1 → 写回主内存
多线程下会出现读写覆盖,最终数据丢失,结果小于预期
解决方案:AtomicInteger(CAS 原子类) /synchronized
3.2 不保证互斥性
volatile 不会阻塞线程,多个线程可以同时操作共享变量,无法实现临界区互斥访问
五、什么是内存屏障
5.1 为什么需要内存屏障
现代 CPU 为了提升执行效率,会进行大量优化
例如下面代码:
java
a = 1;
flag = true;
从程序员角度看:
先执行 a=1
再执行 flag=true
但对于 CPU 来说:只要最终结果一致,执行顺序未必完全按照代码顺序
因此可能发生:
java
flag = true
a = 1
这种现象称为:指令重排序(Instruction Reordering)
单线程环境下通常没有问题,但在多线程环境下可能导致严重 Bug
例如:
线程A:
java
a = 1;
flag = true;
线程B:
java
if (flag) {
System.out.println(a);
}
如果发生重排序:
屏障前面的指令
必须执行完成
↓
才能执行屏障后面的指令
那么线程B可能看到:
java
flag == true
a == 0
最终输出:0
5.2 什么是内存屏障
可以把内存屏障理解成:
CPU执行过程中的"禁止超车线"
例如:
a = 1
══════════════════════
Memory Barrier
══════════════════════
flag = true
内存屏障的作用是:
屏障前面的指令
必须执行完成
↓
才能执行屏障后面的指令
这样 CPU 就不能把:
java
flag = true
提前到:
java
a = 1
之前执行,从而避免多线程环境下的重排序问题
5.3 内存屏障能解决什么问题
内存屏障主要有两个作用。
1、禁止指令重排序
保证:
屏障前的指令
不能跑到屏障后面
屏障后的指令
不能跑到屏障前面
从而保证程序执行顺序符合预期。
2、保证内存可见性
现代 CPU 并不是直接访问主内存
而是:
CPU
↓
CPU Cache(缓存)
↓
Main Memory(主内存)
线程修改变量时:
java
flag = true;
可能先写入 CPU 缓存,其他线程所在 CPU 仍然读取旧值:
java
flag = false;
造成数据不一致,内存屏障会配合 CPU 的缓存一致性协议(MESI):
修改数据
↓
刷新缓存
↓
通知其它CPU缓存失效
↓
重新从主内存读取
从而保证:
一个线程修改的数据
其它线程能够立即看到
这就是 volatile 可见性的来源。
5.4 volatile 如何利用内存屏障
假设:
java
int a = 0;
volatile boolean flag = false;
线程A:
java
a = 1;
flag = true;
线程B:
java
if (flag) {
System.out.println(a);
}
JVM 在编译时会为 volatile 插入内存屏障,执行效果类似:
a = 1
内存屏障
flag = true
这样可以保证:
a=1
一定先执行
↓
flag=true
后执行
同时:
java
flag=true
会立即同步到主内存,线程B读取到:
java
flag == true
JMM 保证:
java
a == 1
也一定可见
因此最终一定输出:1
不会出现:0这种情况
5.5 四种内存屏障(了解)
JMM 定义了四种内存屏障:
| 屏障类型 | 含义 |
|---|---|
| LoadLoad | 读 → 读 |
| StoreStore | 写 → 写 |
| LoadStore | 读 → 写 |
| StoreLoad | 写 → 读 |
5.5.1、LoadLoad
表示:前一个读操作,不能与后一个读操作重排序
Load1
LoadLoad
Load2
保证:
Load1执行完
才能执行Load2
例子:
java
int a = x;
int b = y;
禁止两个读取重排序
5.5.2、StoreStore
表示:前一个写操作,不能与后一个写操作重排序
Store1
StoreStore
Store2
保证:
Store1执行完
才能执行Store2
例子:
java
a = 1;
flag = true;
不能交换
5.5.3、LoadStore
Load
LoadStore
Store
保证:
先读
后写
不能交换
5.5.4、StoreLoad
开销最大,也是最强的一种屏障
Store
StoreLoad
Load
保证:
先写
后读
不能交换,同时触发缓存同步,性能开销最大
volatile 的可见性和有序性,本质上都是 JVM 通过插入内存屏障实现的
volatile 所有能力的底层支撑:四种 CPU 内存屏障,禁止指令跨越屏障重排,同时强制缓存同步
六、底层核心原理:内存屏障
volatile 写屏障(变量赋值时)
执行顺序:StoreStore 屏障 → 变量写操作 → StoreLoad 屏障
-
StoreStore:普通写操作,不能重排到 volatile 写之后
-
StoreLoad(最强屏障):禁止写后重排读操作,强制刷新缓存到主内存
volatile 读屏障(变量读取时)
执行顺序:LoadLoad 屏障 + LoadStore 屏障 → 变量读操作
-
LoadLoad:后续读操作不能重排到本次 volatile 读之前
-
LoadStore:后续写操作不能重排到本次 volatile 读之前
七、JMM Happens-Before 规则
volatile 最核心的 happens-before 规则:
对 volatile 变量的写操作,先行发生于后续任意线程对该变量的读操作
经典示例
java
int a = 0;
volatile boolean flag = false;
// 线程A
a = 1;
flag = true;
// 线程B
if (flag) {
System.out.println(a);
// 必然输出 1,不会出现指令重排结果
}
当线程 B 读取到 flag=true 时,线程 A 中 flag 之前的所有普通变量修改(a=1),全部对线程 B 可见
八、三大经典使用场景
volatile 必须满足:单次写、多次读,无复合操作
6.1 状态标记位(最常用)
用于线程启停、开关控制,依靠可见性实时感知状态变化
java
volatile boolean running = true;
while (running) {
// 循环执行任务
}
6.2 双重校验锁 DCL 单例(禁止重排序)
必须加 volatile 的核心原因:
new Singleton() 正常三步:分配内存 → 初始化对象 → 赋值引用
正常:
1 分配内存
2 初始化对象
3 instance指向内存
重排后:
1 分配内存
2 instance指向内存
3 初始化对象
线程B:
java
if(instance != null)
直接返回了一个未初始化完成对象
无 volatile 会发生指令重排:分配内存 → 赋值引用 → 初始化对象
多线程下其他线程可能获取到未初始化的半初始化对象,引发空指针/异常
java
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
6.3 配置热更新
后台线程实时读取 volatile 修饰的配置变量,配置更新后,所有线程立即感知最新配置
九、Volatile 与 Synchronized 核心对比
| 特性 | volatile | synchronized |
|---|---|---|
| 可见性 | 保证 | 保证 |
| 有序性 | 部分保证(禁止特定重排) | 完全保证 |
| 原子性 | 不保证 | 保证 |
| 互斥性 | 无 | 有(阻塞线程) |
| 性能 | 轻量、无上下文切换 | 重量级、有锁阻塞、上下文切换 |
| 适用场景 | 状态标记、热更新、DCL单例 | 复合操作、临界区资源同步 |
十、性能损耗本质
volatile 比普通变量慢,但远快于锁,损耗来源:
-
强制 CPU 缓存刷新、缓存行失效
-
禁止编译器、CPU 指令重排序优化
-
强制刷写 store buffer 到主内存