volatile介绍

在计算机科学中,"volatile"是一个关键字,用于修饰变量,指示编译器和计算机体系结构在处理该变量时需要特别注意。它主要有两个作用:禁止编译器对该变量的优化,以及保证多线程环境下对该变量的可见性。

当一个变量被声明为"volatile"时,编译器会禁止对该变量进行一些优化操作,例如将变量缓存在寄存器中。这是因为优化操作可能会导致变量的读写顺序发生改变,而这在某些场景下是不可接受的。"volatile"关键字的存在告诉编译器,该变量可能会被其他线程或外部因素修改,因此编译器必须每次都从内存中读取变量的值,并将修改后的值写回内存。

另一个作用是保证"volatile"变量在多线程环境下的可见性。在多线程编程中,不同的线程可能同时访问和修改同一个共享变量。为了确保线程之间能够正确地看到对共享变量的修改,需要使用同步机制,如锁或原子操作。而"volatile"关键字可以用作一种轻量级的同步机制,当一个线程修改了一个"volatile"变量的值时,该变量的新值将立即对其他线程可见。

"volatile"关键字的底层实现原理与具体的编程语言、编译器和计算机体系结构有关。不同的编程语言和编译器可能会采用不同的机制来实现"volatile"的有序性。在某些体系结构中,编译器会插入内存屏障(memory barrier)或禁止重排序指令(ordered load/store instructions)来确保"volatile"变量的有序性。这些机制的具体细节是由底层硬件提供的,例如处理器的内存模型和缓存一致性协议。

需要注意的是,虽然"volatile"可以保证对变量的可见性,但它并不能解决所有的多线程并发问题。在涉及到复杂的线程同步和数据一致性的场景中,仍然需要使用更强大的同步机制,如锁、信号量、条件变量等,来确保线程之间的正确交互和数据一致性。

内存屏障

在底层实现中,"volatile"的有序性通常与处理器的内存模型和编译器优化相关。下面我将简要介绍一些常见的底层实现方式:

  1. 内存屏障(Memory Barriers):内存屏障是一种硬件或指令级别的机制,用于确保在特定位置的指令执行顺序和访问顺序的一致性。在使用"volatile"修饰的变量读写操作周围插入内存屏障可以保证指令不会被重排序,并且所有对内存的读写操作都能按照预期顺序进行。
  2. 编译器优化禁止:编译器在优化代码时可能会对变量的读写顺序进行重新排序,以提高性能。但是当变量被声明为"volatile"时,编译器会禁止对该变量的优化,确保对变量的读写操作按照源代码中的顺序执行。
  3. 缓存一致性协议:多核处理器中,每个核心都有自己的缓存。当多个核心同时访问共享的"volatile"变量时,缓存一致性协议会确保各个核心之间的缓存数据保持一致。当一个核心修改了"volatile"变量的值时,缓存一致性协议会将修改后的值立即更新到其他核心的缓存中,以保证可见性和有序性。
  4. 优化控制:某些编译器提供了特定的编译选项或指令,允许开发人员在需要的时候对"volatile"变量的有序性进行更精确的控制。例如,GCC编译器提供了一些特殊的指令,如"asm volatile",用于指定某个指令序列需要以特定的顺序执行。

需要注意的是,不同的处理器和编译器可能有不同的实现方式。具体的实现细节可能因体系结构、编译器版本和优化级别而异。因此,在使用"volatile"修饰变量时,最好查阅特定编译器和处理器的文档,以了解其具体的有序性保证机制。

StoreStore内存屏障和StoreLoad内存屏障是内存屏障的两个常见类型,它们有不同的作用和语义。

  1. StoreStore内存屏障(也称为Store-Release屏障):
  • 作用:StoreStore屏障用于确保在屏障之前的所有存储操作(写操作)在屏障之后的存储操作之前完成。它保证了存储操作的顺序一致性。
  • 语义:当一个线程在StoreStore屏障之前写入数据,而另一个线程在StoreStore屏障之后进行相应的写操作时,保证后者能够看到前者的最新写入结果。
  1. StoreLoad内存屏障(也称为Store-Load屏障):
  • 作用:StoreLoad屏障用于确保在屏障之前的所有存储操作(写操作)在屏障之后的加载操作(读操作)之前完成。它保证了存储和加载操作之间的顺序一致性。
  • 语义:当一个线程在StoreLoad屏障之前写入数据,而另一个线程在StoreLoad屏障之后读取相同的数据时,保证后者能够看到前者的最新写入结果。

总结:

  • StoreStore屏障确保存储操作在屏障之前完成,存储操作在屏障之后执行。它维护了存储操作的顺序一致性。
  • StoreLoad屏障确保存储操作在屏障之前完成,加载操作在屏障之后执行。它维护了存储和加载操作之间的顺序一致性。

LoadStore内存屏障和LoadLoad内存屏障是内存屏障的两个常见类型,它们有不同的作用和语义。

  1. LoadStore内存屏障(也称为StoreLoad屏障):
  • 作用:LoadStore屏障用于确保在屏障之前的所有存储操作(写操作)在屏障之后的所有加载操作(读操作)之前完成。换句话说,它保证了存储和加载操作的顺序一致性。
  • 语义:当一个线程在LoadStore屏障之前写入数据,而另一个线程在LoadStore屏障之后读取数据时,保证后者能够看到前者的最新写入结果。
  1. LoadLoad内存屏障(也称为Load-Acquire屏障):
  • 作用:LoadLoad屏障用于确保在屏障之前的所有加载操作(读操作)在屏障之后的操作之前完成。它保证了加载操作的顺序一致性。
  • 语义:当一个线程在LoadLoad屏障之前读取数据,而另一个线程在LoadLoad屏障之后读取相同数据时,保证后者能够看到先前读取操作的结果。

总结:

  • LoadStore屏障确保存储操作在屏障之前完成,加载操作在屏障之后执行。它维护了存储和加载操作的顺序一致性。
  • LoadLoad屏障确保加载操作在屏障之前完成。它维护了加载操作的顺序一致性。

这些内存屏障类型在多线程编程中非常重要,因为它们提供了对共享数据的正确同步和可见性保证。使用适当的内存屏障可以防止数据竞争、重排序和不一致性等并发问题。但是,具体的内存屏障行为和语义可能因编程语言、编译器和处理器的不同而有所差异,因此在编写并发代码时,建议查阅相关文档来了解具体的内存屏障行为和使用方式。


被volatile修饰的属性,在编译时,会在前后追加 内存屏障 。

SS:屏障前的读写操作,必须全部完成,再执行后续操作

SL:屏障前的写操作,必须全部完成,再执行后续读操作

LL:屏障前的读操作,必须全部完成,再执行后续读操作

LS:屏障前的读操作,必须全部完成,再执行后续写操作

java volatile的有序性底层实现原理_java

这个内存屏障是JDK规定的,目的是保证volatile修饰的属性不会出现指令重排的问题。

volatile在JMM层面,保证JIT不重排可以理解,但是,CPU怎么实现的。

查看这个文档:https://gee.cs.oswego.edu/dl/jmm/cookbook.html

java volatile的有序性底层实现原理_加载_02

不同的CPU对内存屏障都有一定的支持,比如×86架构,内部自己已经实现了LS,LL,SS,只针对SL做了支持。

去openJDK再次查看,mfence是如何支持的。其实在底层还是mfence内部的lock指定,来解决指令重排问题。

java volatile的有序性底层实现原理_加载_03