volatile 关键字的底层实现原理

本文深入探讨了Java并发编程中的volatile关键字,解释了其如何保证多线程环境下共享变量的可见性,以及与synchronized的区别和优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参考书籍:《Java 并发编程的艺术》 - 方腾飞 魏鹏 程晓明著

在并发编程中,synchronized 和 volatile 这两个关键字都扮演着重要的角色,volatile 是轻量级的 synchronized,它在多处理器开发中保证了共享变量的 “可见性”。

那么什么是 “可见性” 呢?

可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值

如果 volatile 使用得当的话,它会比 synchronized 的成本更低,因为它不会造成线程的阻塞,也就不会导致上下文切换和调度,所以性能更好,开销更低。
 

volatile 定义

Java 语言规范第 3 版对 volatile 的定义如下:

Java 编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排它锁单独获得这个变量。

Java 提供了 volatile,在某些情况下比锁要更加方便。

如果一个字段被声明为 volatile,Java 的线程内存模型(JMM)确保所有线程看到这个变量的值是一致的。

 

volatile 实现原理

看一个简单的例子:

private volatile Singleton instance = null;

public Singleton getInstance(){
        if(instance == null){
            instance = new Singleton(); 
        }
        return instance;
}

instance 这个成员变量是被 volatile 关键字修饰的,所以 instance 这个引用的改变对于各个线程来说是 “可见的”。那么 volatile 是如何保证可见性的呢?

我们在 x86 处理器下通过工具获取 JIT 编译器生成汇编指令来查看对 volatile 进行写操作时,CPU 会做什么事情(就是这句代码:instance = new Singleton();)。

转成汇编代码如下:

0x01a3deld: movb $0x0,0x1104800(%esi);0x01a3de24: lock add1 $0x0,(%esp);

有 volatile 变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,Lock 前缀的指令在多核处理器下会引发两件事情:

  • 将当前处理器缓存行的数据写回到系统内存。
  • 这个写回内存的会使在其他 CPU 里缓存了该内存地址的数据无效。

正是这两个额外的事件,使得 volatile 保证了共享变量的 “可见性”。

<think>我们正在讨论volatile关键字如何通过内存屏障和缓存一致性实现可见性。 根据引用内容,我们可以总结如下: 1. 可见性的实现主要依靠两方面: a) 缓存一致性协议(如MESI) b) 内存屏障 2. 缓存一致性协议(如MESI)确保多个处理器核心之间缓存的一致性。 - 当一个线程修改了volatile变量,这个修改会立即被写回主内存(实际上是写回高速缓存,然后由缓存一致性协议保证其他缓存失效或更新)。 - 其他处理器通过总线嗅探机制(Bus Snooping)感知到该修改,并使自己缓存中对应的缓存行失效(Invalidate)。 - 这样,其他线程在读取该变量时,会发现缓存行已失效,从而从主内存(或通过缓存一致性协议从其他缓存)重新加载最新值。 3. 内存屏障则用于确保: a) 立即将写缓冲区的数据刷新到缓存(进而通过缓存一致性协议刷新到主内存) b) 立即将失效队列中的失效消息处理掉,保证后续读取时能拿到最新值。 具体来说: - 写屏障(Store Barrier):确保屏障之前的写操作(包括volatile写)的结果在屏障之后对其他处理器可见。具体来说,它会强制将写缓冲区(Store Buffer)中的数据刷新到高速缓存(或主内存),并等待失效确认(Invalidate Acknowledge)完成。 - 读屏障(Load Barrier):确保屏障之后的读操作(包括volatile读)能够获取到最新的值。具体来说,它会强制处理器在处理读操作之前,先处理完失效队列(Invalidate Queue)中的所有失效消息,从而使该处理器缓存中相应的缓存行失效,进而从主内存(或其他缓存)中重新加载。 4. 对于volatile变量,JVM会在其写操作之后插入一个写屏障,在读操作之前插入一个读屏障。 因此,volatile关键字的可见性实现可以总结为以下步骤: 步骤1(写操作): 1.1 线程A修改volatile变量,写入自己的本地缓存(或写缓冲区)。 1.2 由于写屏障的作用,线程A必须等待该写操作被刷新到缓存(并最终到主内存)且收到所有其他处理器的失效确认(即该缓存行在其他缓存中的副本都被置为失效状态)后才能继续执行后续操作。 步骤2(读操作): 2.1 线程B读取volatile变量,由于读屏障的作用,它会先处理失效队列,将本地缓存中该变量的缓存行置为失效。 2.2 由于缓存行失效,线程B会从主内存(或通过缓存一致性协议从其他缓存)重新加载最新值。 这样,线程A的写操作结果对线程B就是可见的了。 下面我们用图示和代码说明: 图示: ```mermaid graph LR A[线程A:写volatile变量] --> B[插入写屏障] B --> C[强制刷新写缓冲区到缓存] C --> D[通过MESI协议使其他缓存行失效] D --> E[等待失效确认] E --> F[继续执行] G[线程B:读volatile变量] --> H[插入读屏障] H --> I[处理失效队列] I --> J[缓存行失效,重新从主内存加载] ``` 引用说明: - 内存屏障(写屏障和读屏障)的作用在引用[2]和引用[4]中有描述。 - 缓存一致性协议(MESI)在引用[1]和引用[4]中提及。 注意:这里的内存屏障是硬件层面的,JVM在编译时会在volatile写操作后插入写屏障指令,在volatile读操作前插入读屏障指令。 因此,volatile关键字的可见性是通过内存屏障强制刷新缓存和使缓存失效,以及缓存一致性协议(如MESI)共同作用来实现的。 最后,我们生成相关问题。</think>### volatile 关键字如何通过内存屏障和缓存一致性实现可见性 #### 一、核心实现机制 volatile 的可见性通过 **硬件级缓存一致性协议** 和 **内存屏障** 协同实现,具体流程如下: 1. **缓存一致性协议(MESI)** - **总线嗅探(Bus Snooping)**: 当线程 A 修改 volatile 变量时,CPU 通过总线广播 `invalidate` 信号,通知其他核心的缓存失效[^4]。 $$ \text{volatile写} \rightarrow \text{总线广播} \rightarrow \text{其他缓存行失效} $$ - **失效队列处理**: 其他线程(如线程 B)在读取 volatile 变量前,必须处理失效队列中的 `invalidate` 消息,确保本地缓存副本无效[^4]。 ```mermaid graph LR A[线程A写volatile变量] --> B[强制刷回主内存] B --> C[总线广播invalidate信号] C --> D[线程B的缓存行失效] D --> E[线程B从主内存重新加载] ``` 2. **内存屏障(Memory Barrier)** - **写屏障(Store Barrier)**: 插入在 volatile 写操作之后,强制将写缓冲区(Store Buffer)数据刷回高速缓存/主内存[^4]。 ```java public void writeVolatile() { data = 42; // volatile写 // 隐式插入写屏障(StoreStore + StoreLoad) } ``` - **读屏障(Load Barrier)**: 插入在 volatile 读操作之前,强制处理失效队列并刷新本地缓存[^2]。 ```java public int readVolatile() { // 隐式插入读屏障(LoadLoad + LoadStore) return data; // volatile读 } ``` 3. **屏障组合实现可见性** | 操作序列 | 插入屏障类型 | 作用 | |------------------|----------------------------|----------------------------------------------------------------------| | volatile写 | `StoreStore` + `StoreLoad` | 确保写操作前的修改对所有线程可见,并防止与后续操作重排序[^1] | | volatile读 | `LoadLoad` + `LoadStore` | 确保后续操作能读到最新值,并防止与之前操作重排序[^2] | --- #### 二、底层硬件协作流程 以 **x86 架构** 为例的完整可见性实现: 1. **写操作流程**: ```mermaid sequenceDiagram 线程A->>L1缓存: 修改volatile变量 L1缓存->>总线: 广播invalidate信号 总线->>其他CPU缓存: 使缓存行失效 L1缓存->>主内存: 通过Store屏障强制刷回数据 ``` - 关键指令:`lock addl $0x0,(%rsp)`(x86的StoreLoad屏障实现)[^1] 2. **读操作流程**: ```mermaid sequenceDiagram 线程B->>失效队列: 处理invalidate消息(Load屏障) 失效队列->>L1缓存: 标记缓存行为I(Invalid) 线程B->>主内存: 重新加载最新数据 ``` - 关键机制:**MESI协议状态转换**(Modified→Exclusive→Shared→Invalid)[^3] --- #### 三、可见性保障的本质 1. **强制主内存同步** - volatile 变量读写直接穿透 CPU 缓存层级,避免线程工作内存与主内存不一致[^1]。 $$ \text{volatile读写} \equiv \text{绕过工作内存直接访问主内存} $$ 2. **屏障的刷新语义** | 屏障类型 | 伪代码实现 | 作用 | |------------|-----------------------|-------------------------------| | 写屏障 | `flushStoreBuffer()` | 清空写缓冲区到缓存/内存[^4] | | 读屏障 | `processInvalidateQueue()` | 处理失效队列刷新缓存[^4] | 3. **与普通变量对比** ```java // 普通变量(无可见性保证) int a = 1; // 可能停留在写缓冲区或工作内存 // volatile变量(强制可见) volatile int b = 2; // 立即刷回主内存并通知其他缓存失效 ``` --- ### 总结:可见性实现三要素 1. **缓存失效机制**:通过 MESI 协议使其他核心缓存失效[^3] 2. **内存屏障强制刷新**: - 写屏障 → 数据刷回主内存 - 读屏障 → 加载最新数据[^2] 3. **禁止重排序**:确保操作顺序符合程序语义[^1] > ⚠️ **性能代价**:频繁 volatile 操作会触发大量缓存失效和屏障指令,在超线程环境下可能引发 **全局存储屏障(MFENCE)**,导致性能下降[^4]。 --- ### 相关问题 1. **MESI 协议中缓存行的四种状态如何转换?失效队列满时会发生什么?** 2. **x86 架构的 `LOCK` 指令前缀与 volatile 的 StoreLoad 屏障有何关系?** 3. **为什么 Java 不直接使用 `fence` 指令而用 `lock addl $0x0,(%rsp)` 实现屏障?** 4. **ZGC/Shenandoah 等新垃圾收集器如何优化 volatile 的内存屏障开销?** 5. **ARM 架构的 `DMB`/`DSB` 指令与 x86 内存屏障的映射关系是什么?**
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值