volatile底层原理详解

本文深入探讨了Java中的volatile关键字,它提供了轻量级的同步机制,确保并发环境下的变量可见性和有序性。volatile不保证原子性,但通过内存屏障保证了数据同步。详细分析了volatile在JMM中的实现,包括内存模型、字节码、JVM源码、汇编及硬件层面的实现,揭示了其背后保证可见性和禁止指令重排序的原理。

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

volatile关键字是Java虚拟机提供的最轻量级的同步机制。在多线程编程中volatilesynchronized都起着举足轻重的作用,没有这两者,也就没有那么多JUC供我们使用。

本文会介绍volatile的作用,着重讲解volatile的底层实现原理。由于volatile的出现和CPU缓存有关,也会介绍CPU缓存的相关内容,让我们更清晰的理解volatile原理的来龙去脉。

一、volatile的作用

并发编程中有3大重要特性,了解一下:

  • 原子性

一个操作或者多个操作,要么全部执行成功,要么全部执行失败。满足原子性的操作,中途不可被中断。

  • 可见性

多个线程共同访问共享变量时,某个线程修改了此变量,其他线程能立即看到修改后的值。

  • 有序性

程序执行的顺序按照代码的先后顺序执行。(由于JMM模型中允许编译器和处理器为了效率,进行指令重排序的优化。指令重排序在单线程内表现为串行语义,在多线程中会表现为无序。那么多线程并发编程中,就要考虑如何在多线程环境下可以允许部分指令重排,又要保证有序性)

synchronized关键字同时保证上述三种特性。

  • synchronized是同步锁,同步块内的代码相当于同一时刻单线程执行,故不存在原子性和指令重排序的问题
  • synchronized关键字的语义JMM有两个规定,保证其实现内存可见性:
  • 线程解锁前,必须把共享变量的最新值刷新到主内存中;
  • 线程加锁前,将清空工作内存中共享变量的值,从主内存中冲洗取值。

volatile关键字作用的是保证可见性有序性,并不保证原子性。

那么,volatile是如何保证可见性有序性的?我们先进行基于JMM层面的实现基础,后面两章会进行底层原理的介绍。

1.1、volatile变量的可见性

Java虚拟机规范中定义了一种Java内存 模型(Java Memory Model,即JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。Java内存模型的主要目标就是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的细节

JMM中规定所有的变量都存储在主内存(Main Memory)中,每条线程都有自己的工作内存(Work Memory),线程的工作内存中保存了该线程所使用的变量的从主内存中拷贝的副本。线程对于变量的读、写都必须在工作内存中进行,而不能直接读、写主内存中的变量。同时,本线程的工作内存的变量也无法被其他线程直接访问,必须通过主内存完成。

整体内存模型如下图所示:

 

 

对于普通共享变量,线程A将变量修改后,体现在此线程的工作内存。在尚未同步到主内存时,若线程B使用此变量,从主内存中获取到的是修改前的值,便发生了共享变量值的不一致,也就是出现了线程的可见性问题

volatile定义:

  • 当对volatile变量执行写操作后,JMM会把工作内存中的最新变量值强制刷新到主内存
  • 写操作会导致其他线程中的缓存无效

这样,其他线程使用缓存时,发现本地工作内存中此变量无效,便从主内存中获取,这样获取到的变量便是最新的值,实现了线程的可见性。

1.2、volatile变量的禁止指令重排序

volatile是通过编译器在生成字节码时,在指令序列中添加“内存屏障”来禁止指令重排序的。

硬件层面的“内存屏障”:

  • sfence:即写屏障(Store Barrier),在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存,以保证写入的数据立刻对其他线程可见
  • lfence:即读屏障(Load Barrier),在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据,以保证读取的是最新的数据。
  • mfence:即全能屏障(modify/mix Barrier ),兼具sfence和lfence的功能
  • lock 前缀:lock不是内存屏障,而是一种锁。执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。

JMM层面的“内存屏障”:

  • LoadLoad屏障: 对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreLoad屏障: 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

JVM的实现会在volatile读写前后均加上内存屏障,在一定程度上保证有序性。如下所示:

LoadLoadBarrier
volatile 读操作
LoadStoreBarrier

StoreStoreBarrier
volatile 写操作
StoreLoadBarrier

二、volatile的的底层实现

这一章会从Java代码、字节码、Jdk源码、汇编层面、硬件层面去揭开volatile的面纱。

2.1、 Java代码层面

上一段最简单的代码,volatile用来修饰Java变量

public class TestVolatile {

    public static volatile int counter = 1;

    public static void main(String[] args){
        counter = 2;
        System.out.println(counter);
    }
}

2.2、字节码层面

通过javac TestVolatile.java将类编译为class文件,再通过javap -v TestVolatile.class命令反编译查看字节码文件。

打印内容过长,截图其中的一部分:

 

 

可以看到,修饰counter字段的public、static、volatile关键字,在字节码层面分别是以下访问标志: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE

volatile在字节码层面,就是使用访问标志:ACC_VOLATILE来表示,供后续操作此变量时判断访问标志是否为ACC_VOLATILE,来决定是否遵循volatile的语义处理。

2.3、JVM源码层面

上小节图中main方法编译后的字节码,有putstaticgetstatic指令(如果是非静态变量,则对应putfieldgetfield指令)来操作counter字段。那么对于被volatile变量修饰的字段,是如何实现volatile语义的,从下面的源码看起。

1、openjdk8根路径/hotspot/src/share/vm/interpreter路径下的bytecodeInterpreter.cpp文件中,处理putstaticputfield指令的代码:

CASE(_putfield):
CASE(_putstatic):
    {
          // .... 省略若干行 
          // ....

          // Now store the result 现在要开始存储结果了
          // ConstantPoolCacheEntry* cache;     -- cache是常量池缓存实例
          // cache->is_volatile()               -- 判断是否有volatile访问标志修饰
          int field_offset = cache->f2_as_index();
          if (cache->is_volatile()) { // ****重点判断逻辑**** 
            // volatile变量的赋值逻辑
            if (tos_type == itos) {
              obj->release_int_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == atos) {// 对象类型赋值
              VERIFY_OOP(STACK_OBJECT(-1));
              obj->release_obj_field_put(field_offset, STACK_OBJECT(-1));
              OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
            } else if (tos_type == btos) {// byte类型赋值
              obj->release_byte_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ltos) {// long类型赋值
              obj->release_long_field_put(field_offset, STACK_LONG(-1));
            } else if (tos_type == ctos) {// char类型赋值
              obj->release_char_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == stos) {// short类型赋值
              obj->release_short_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ftos) {// float类型赋值
              obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
            } else {// double类型赋值
              obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
            }
            // *** 写完值后的storeload屏障 ***
            OrderAccess::storeload();
          } else {
            // 非volatile变量的赋值逻辑
            if (tos_type == itos) {
              obj->int_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == atos) {
              VERIFY_OOP(STACK_OBJECT(-1));
              obj->obj_field_put(field_offset, STACK_OBJECT(-1));
              OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
            } else if (tos_type == btos) {
              obj->byte_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ltos) {
              obj->long_field_put(field_offset, STACK_LONG(-1));
            } else if (tos_type == ctos) {
              obj->char_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == stos) {
              obj->short_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ftos) {
              obj->float_field_put(field_offset, STACK_FLOAT(-1));
            } else {
              obj->double_field_put(field_offset, STACK_DOUBLE(-1));
            }
          }
          UPDATE_PC_AND_TOS_AND_CONTINUE(3, count);
  }

2、重点判断逻辑cache->is_volatile()方法,调用的是openjdk8根路径/hotspot/src/share/vm/utilities路径下的accessFlags.hpp文件中的方法,用来判断访问标记是否为volatile修饰

// Java access flags
  bool is_public      () const         { return (_flags & JVM_ACC_PUBLIC      ) != 0; }
  bool is_private     () const         { return (_flags & JVM_ACC_PRIVATE     ) != 0; }
  bool is_protected   () const         { return (_flags & JVM_ACC_PROTECTED   ) != 0; }
  bool is_static      () const         { return (_flags & JVM_ACC_STATIC      ) != 0; }
  bool is_final       () const         { return (_flags & JVM_ACC_FINAL       ) != 0; }
  bool is_synchronized() const         { return (_flags & JVM_ACC_SYNCHRONIZED) != 0; }
  bool is_super       () const         { return (_flags & JVM_ACC_SUPER       ) != 0; }
  // 是否volatile修饰
  bool is_volatile    () const         { return (_flags & JVM_ACC_VOLATILE    ) != 0; }
  bool is_transient   () const         { return (_flags & JVM_ACC_TRANSIENT   ) != 0; }
  bool is_native      () const         { return (_flags & JVM_ACC_NATIVE      ) != 0; }
  bool is_interface   () const         { return (_flags & JVM_ACC_INTERFACE   ) != 0; }
  bool is_abstract    () const         { return (_flags & JVM_ACC_ABSTRACT    ) != 0; }
  bool is_strict      () const         { return (_flags & JVM_ACC_STRICT      ) != 0; }

3、下面一系列的if...else...对tos_type字段的判断处理,是针对java基本类型和引用类型的赋值处理。如:

obj->release_byte_field_put(field_offset, STACK_INT(-1));

对byte类型的赋值处理,调用的是openjdk8根路径/hotspot/src/share/vm/oops路径下的oop.inline.hpp文件中的方法:

// load操作调用的方法
inline jbyte oopDesc::byte_field_acquire(int offset) const                  
{ return OrderAccess::load_acquire(byte_field_addr(offset));     }
// store操作调用的方法
inline void oopDesc::release_byte_field_put(int offset, jbyte contents)     
{ OrderAccess::release_store(byte_field_addr(offset), contents); }

赋值的操作又被包装了一层,又调用的OrderAccess::release_store方法。

4、OrderAccess是定义在openjdk8根路径/hotspot/src/share/vm/runtime路径下的orderAccess.hpp头文件下的方法,具体的实现是根据不同的操作系统和不同的cpu架构,有不同的实现。

强烈建议大家读一遍orderAccess.hpp文件中30-240行的注释!!!你就会发现本文1.2章所介绍内容的来源,也是网上各种雷同文章的来源。

 

orderAccess_linux_x86.inline.hpp是linux系统下x86架构的实现:

 

 

可以从上面看到,到c++的实现层面,又使用c++中的volatile关键字,用来修饰变量,通常用于建立语言级别的memory barrier。在《C++ Programming Language》一书中对volatile修饰词的解释:

A volatile specifier is a hint to a compiler that an object may change its value in ways not specified by the language so that aggressive optimizations must be avoided.

含义就是:

  • volatile修饰的类型变量表示可以被某些编译器未知的因素更改(如:操作系统,硬件或者其他线程等)
  • 使用 volatile 变量时,避免激进的优化。即:系统总是重新从内存读取数据,即使它前面的指令刚从内存中读取被缓存,防止出现未知更改和主内存中不一致

5、步骤3中对变量赋完值后,程序又回到了2.3.1小章中第一段代码中一系列的if...else...对tos_type字段的判断处理之后。有一行关键的代码:OrderAccess::storeload(); 即:只要volatile变量赋值完成后,都会走这段代码逻辑。

它依然是声明在orderAccess.hpp头文件中,在不同操作系统或cpu架构下有不同的实现。orderAccess_linux_x86.inline.hpp是linux系统下x86架构的实现:

 

 

代码lock; addl $0,0(%%rsp) 其中的addl $0,0(%%rsp) 是把寄存器的值加0,相当于一个空操作(之所以用它,不用空操作专用指令nop,是因为lock前缀不允许配合nop指令使用)

lock前缀,会保证某个处理器对共享内存(一般是缓存行cacheline,这里记住缓存行概念,后续重点介绍)的独占使用。它将本处理器缓存写入内存,该写入操作会引起其他处理器或内核对应的缓存失效。通过独占内存、使其他处理器缓存失效,达到了“指令重排序无法越过内存屏障”的作用

2.4、汇编层面

运行2.1章的代码时,加上JVM的参数:-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly,就可以看到它的汇编输出。(如果运行报错,参见上篇文章:synchronized底层原理(从Java对象头说到即时编译优化),拉到文章最底部有解决方案)

打印的汇编代码较长,仅截取其中的关键部分:

 

 

又看到了lock addl $0x0,(%rsp)指令,熟悉的配方熟悉的味道,和上面2.3章中的步骤5一摸一样,其实这里就是步骤5中代码的体现。

2.5、硬件层面

为什么会有上述如此复杂问题?为什么会有并发编程?为什么会产生可见性、有序性、原子性的线程或内存问题?

归根结底,还是计算机硬件高速发展的原因。CPU架构的发展,多核CPU的产生以及CPU缓存的适配是导致并发问题产生的原因之一。

CPU缓存相关内容也是一大块内容,消化完上述的干货内容,请看下一篇对CPU缓存相关的干货文章。

<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、付费专栏及课程。

余额充值