【底层】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 缓存与主内存的交互顺序可能与程序顺序不一致(例如写缓冲区的刷新顺序)。

相关推荐
weixin_9854321130 分钟前
Spring Boot 中的 @ConditionalOnBean 注解详解
java·spring boot·后端
猎人everest1 小时前
快速搭建运行Django第一个应用—投票
后端·python·django
啾啾Fun3 小时前
精粹汇总:大厂编程规范(持续更新)
后端·规范
yt948323 小时前
lua读取请求体
后端·python·flask
IT_10243 小时前
springboot从零入门之接口测试!
java·开发语言·spring boot·后端·spring·lua
汪子熙4 小时前
在 Word 里编写 Visual Basic 调用 DeepSeek API
后端·算法·架构
寻月隐君5 小时前
手把手教你用 Solana Token-2022 创建支持元数据的区块链代币
后端·web3·github
代码丰5 小时前
使用Spring Cloud Stream 模拟生产者消费者group destination的介绍(整合rabbitMQ)
java·分布式·后端·rabbitmq
烛阴5 小时前
Cheerio DOM操作深度指南:轻松玩转HTML元素操作
前端·javascript·后端
Hello.Reader6 小时前
在多云环境透析连接ngx_stream_proxy_protocol_vendor_module
后端·python·flask