【底层】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 小时前
Java-80 深入浅出 RPC Dubbo 动态服务降级:从雪崩防护到配置中心秒级生效
java·分布式·后端·spring·微服务·rpc·dubbo
舒一笑5 小时前
我的开源项目-PandaCoder迎来史诗级大更新啦
后端·程序员·intellij idea
@昵称不存在6 小时前
Flask input 和datalist结合
后端·python·flask
zhuyasen6 小时前
Go 分布式任务和定时任务太难?sasynq 让异步任务从未如此简单
后端·go
东林牧之7 小时前
Django+celery异步:拿来即用,可移植性高
后端·python·django
超浪的晨7 小时前
Java UDP 通信详解:从基础到实战,彻底掌握无连接网络编程
java·开发语言·后端·学习·个人开发
AntBlack8 小时前
从小不学好 ,影刀 + ddddocr 实现图片验证码认证自动化
后端·python·计算机视觉
Pomelo_刘金8 小时前
Clean Architecture 整洁架构:借一只闹钟讲明白「整洁架构」的来龙去脉
后端·架构·rust
双力臂4048 小时前
Spring Boot 单元测试进阶:JUnit5 + Mock测试与切片测试实战及覆盖率报告生成
java·spring boot·后端·单元测试
midsummer_woo10 小时前
基于spring boot的医院挂号就诊系统(源码+论文)
java·spring boot·后端