JMM 进阶:彻底理解 volatile 实现原理

目录

一、什么是volatile

[二、为什么需要 volatile](#二、为什么需要 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 到主内存

相关推荐
晚风吹红霞1 小时前
C++模板进阶:非类型参数、特化、分离编译与优缺点解析
开发语言·c++
Yeats_Liao1 小时前
Java网络编程(五):Selector选择器与高并发实现
java·后端·架构
小小龙学IT1 小时前
Go语言后端开发入门指南
开发语言·后端·golang
不会C语言的男孩1 小时前
C++ Primer 第8章:IO 库
开发语言·c++
AC赳赳老秦1 小时前
OpenClaw任务复盘自动化:统计每日完成工作、遗留问题,优化工作节奏
java·大数据·linux·运维·服务器·数据库·openclaw
兰令水2 小时前
leecodecode【层序遍历】【2026.6.3打卡-java版本】
java·开发语言
Halo_tjn2 小时前
反射与设计模式2
java·开发语言·算法
YDS8292 小时前
DeepSeek RAG&MCP + Agent智能体项目 —— 动态决策策略的接口对接
java·spring boot·ai·agent·spring ai·deepseek
zfoo-framework2 小时前
跨服架构设计模式(同构进程+选主转发)
java