【底层】Volatile的理解

www.bilibili.com/video/BV1WC...

主要作用

作用:

1.保证线程的可见性

2.禁止指令重排

volatile 关键字如何保证线程的可见性和禁止指令重排序,其底层实现主要依赖 内存屏障(Memory Barriers) 和 缓存一致性协议(如 MESI),具体机制如下:

1. 保证线程的可见性

强制读写主内存:volatile 变量的读写直接操作主内存(而不是线程的工作内存),确保修改对其他线程立即可见。

内存屏障:插入 Load 和 Store 屏障,保证:

写操作后:强制将工作内存的修改刷新到主内存(Store 屏障)。

读操作前:强制从主内存重新加载最新值(Load 屏障)。

缓存一致性协议:通过 CPU 的缓存一致性协议(如 MESI),监听总线上的数据修改,使其他线程的缓存行失效,强制重新从主内存加载最新值。

2. 禁止指令重排序 原理:

内存屏障插入规则:

写屏障(Store Barrier):在 volatile 写操作后插入,确保该写操作前的所有操作不会被重排到写之后。

读屏障(Load Barrier):在 volatile 读操作前插入,确保该读操作后的所有操作不会被重排到读之前。

具体屏障类型:

StoreStore 屏障:禁止普通写与 volatile 写重排序。

StoreLoad 屏障:禁止 volatile 写与后续的 volatile 读/写重排序。

LoadLoad + LoadStore 屏障:禁止 volatile 读与普通读/写重排序。

示例:双重检查锁单例模式

java

public class Singleton {

private static volatile Singleton instance;

csharp 复制代码
public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton(); // 无指令重排风险
            }
        }
    }
    return instance;
}

}

无 volatile 的问题:对象初始化可能被重排序为"分配内存 → 返回引用 → 初始化"(其他线程可能拿到未初始化的对象)。

volatile 的作用:通过内存屏障禁止指令重排,保证"分配内存 → 初始化 → 返回引用"的顺序。

3. 底层实现总结

**特性 实现机制

****可见性 内存屏障强制刷新主内存 + 缓存失效监听(MESI)

****有序性 插入内存屏障禁止编译器和 CPU 重排序

****原子性 不保证(如 volatile int i; i++ 是非原子操作)

**volatile 通过硬件和 JVM 的协作,以性能损耗为代价,实现轻量级的线程安全控制。

可见性【代码演示】

以下是用 Java 代码演示 线程可见性问题 的示例。当没有 volatile 关键字时,线程间对共享变量的修改可能不可见:

java 复制代码
public class VisibilityDemo {

    // 共享变量(无 volatile 修饰)
    private static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        // 线程 A:等待 flag 变为 true
        Thread threadA = new Thread(() -> {
            while (!flag) {
                // 空循环(模拟等待)
            }
            System.out.println("Thread A: Flag is now true!");
        });

        // 线程 B:修改 flag 为 true
        Thread threadB = new Thread(() -> {
            flag = true;
            System.out.println("Thread B: Set flag to true.");
        });

        threadA.start();
        // 确保线程 A 先启动
        Thread.sleep(100);
        threadB.start();

        // 等待线程结束
        threadA.join();
        threadB.join();
    }
}

没有 volatile 时:

  1. 可能现象 :线程 A 的循环永远不会退出(即使线程 B 已将 flag 设置为 true)。
  2. 原因 :线程 A 在自己的工作内存中缓存了 flag 的初始值 false,无法感知到线程 B 的修改。

添加 volatile 后:

arduino 复制代码
private static volatile boolean flag = false; // 添加 volatile
  1. 现象 :线程 A 会立即退出循环,打印 Thread A: Flag is now true!
  2. 原因volatile 强制线程每次访问 flag 时都从主内存读取最新值。

可见性问题本质

  • JMM(Java 内存模型) :每个线程有自己的工作内存,默认情况下不保证共享变量的修改对其他线程立即可见。
  • volatile 的强制刷新:通过内存屏障强制线程从主内存读写共享变量。

扩展测试:观察延迟问题

即使没有 volatile,某些情况下程序可能"偶然"正常退出(例如循环体中有其他操作触发了内存刷新)。但这是不可靠的,实际代码中必须显式保证可见性:

arduino 复制代码
// 不可靠的写法(可能偶尔正常)
while (!flag) {
    // 添加以下代码可能意外触发内存刷新(但不要依赖这种写法!)
    // System.out.println("Waiting...");
    // Thread.sleep(1);
}

总结

场景 结果 原因
volatile 可能无限循环 线程间不可见性
volatile 立即退出循环 强制主内存读写

通过这个示例可以直观理解 volatile 对可见性的保障作用。

指令重排

  1. 为什么会指令重排?

单线程下,指令1和指令2的执行顺序可以互换,因为它们的操作是独立的。重排后不会影响最终结果,但能提高 CPU 流水线的执行效率(例如减少指令等待时间)。

1. 编译器优化重排

编译器在生成字节码时,会调整指令顺序以提高性能:

ini 复制代码
// 原始代码
int x = 1;
int y = 2;

// 编译器可能重排后的字节码顺序
int y = 2;
int x = 1;  // 顺序互换,但结果不变

2. CPU 指令级并行重排

现代 CPU 采用流水线、多发射等技术,可能并行执行没有依赖关系的指令:

ini 复制代码
int a = 10;          // 指令A
int b = 20;          // 指令B
int result = a * b;  // 指令C

CPU 可能同时执行指令A和指令B,再执行指令C。

3. 内存系统重排

CPU 缓存与主内存的交互顺序可能与程序顺序不一致(例如写缓冲区的刷新顺序)。

相关推荐
顺丰同城前端技术团队4 分钟前
DeepSeek 国产大模型新标杆
前端·后端·程序员
YaHuiLiang19 分钟前
小微互联网公司与互联网创业公司 -- 学历之殇
前端·后端·面试
冬天的风滚草22 分钟前
Higress开源版 大规模 MCP Server 部署配置方案
后端
雨落倾城夏未凉22 分钟前
4.信号与槽
后端·qt
考虑考虑1 小时前
JDK9中的dropWhile
java·后端·java ee
martinzh3 小时前
Spring AI 项目介绍
后端
前端付豪3 小时前
20、用 Python + API 打造终端天气预报工具(支持城市查询、天气图标、美化输出🧊
后端·python
爱学习的小学渣3 小时前
关系型数据库
后端
武子康3 小时前
大数据-33 HBase 整体架构 HMaster HRegion
大数据·后端·hbase
前端付豪3 小时前
19、用 Python + OpenAI 构建一个命令行 AI 问答助手
后端·python