volatile(三)

简介: volatile 这个关键字大家都不陌生,这个关键字一般通常用于并发编程中,是 Java 虚拟机提供的轻量化同步机制,你可能知道 volatile 是干啥的,但是你未必能够清晰明了的知道 volatile 的实现机制,以及 volatile 解决了什么问题,这篇文章我就来带大家解析一波。

这里我们先来了解一下内存屏障的概念。

内存屏障也叫做栅栏,它是一种底层原语。它使得 CPU 或编译器在对内存进行操作的时候, 要严格按照一定的顺序来执行, 也就是说在 memory barrier 之前的指令和 memory barrier 之后的指令不会由于系统优化等原因而导致乱序。

内存屏障提供了两个功能。首先,它们通过确保从另一个 CPU 来看屏障的两边的所有指令都是正确的程序顺序;其次它们可以实现内存数据可见性,确保内存数据会同步到 CPU 缓存子系统。

不同计算机体系结构下面的内存屏障也不一样,通常需要认真研读硬件手册来确定,所以我们的主要研究对象是基于 x86 的内存屏障,通常情况下,硬件为我们提供了四种类型的内存屏障。

  • LoadLoad 屏障

它的执行顺序是 Load1 ;LoadLoad ;Load2 ,其中的 Load1 和 Load2 都是加载指令。LoadLoad 指令能够确保执行顺序是在 Load1 之后,Load2 之前,LoadLoad 指令是一个比较有效的防止看到旧数据的指令。

  • StoreStore 屏障

它的执行顺序是 Store1 ;StoreStore ;Store2 ,和上面的 LoadLoad 屏障的执行顺序相似,它也能够确保执行顺序是在 Store1 之后,Store2 之前。

  • LoadStore 屏障

它的执行顺序是 Load1 ;StoreLoad ;Store2 ,保证 Load1 的数据被加载在与这数据相关的 Store2 和之后的 store 指令之前。

  • StoreLoad 屏障

它的执行顺序是 Store1 ;StoreLoad ;Load2 ,保证 Store1 的数据被其他 CPU 看到,在数据被 Load2 和之后的 load 指令加载之前。也就是说,它有效的防止所有 barrier 之前的 stores 与所有 barrier 之后的 load 乱序。

JMM 采取了保守策略来实现内存屏障,JMM 使用的内存屏障如下

微信图片_20220416173928.png

下面是一个使用内存屏障的示例

class MemoryBarrierTest {
  int a, b;
  volatile int v, u;
  void f() {
    int i, j;
    i = a;
    j = b;
    i = v;
    j = u;
    a = i;
    b = j;
    v = i;
    u = j;
    i = u;
    j = b;
    a = i;
  }
}

这段代码虽然比较简单,但是使用了不少变量,看起来有些乱,我们反编译一下来分析一下内存屏障对这段代码的影响。

微信图片_20220416173932.png

从反编译的代码我们是看不到内存屏障的,因为内存屏障是一种硬件层面的指令,单凭字节码是肯定无法看到的。虽然无法看到内存屏障的硬件指令,但是 JSR-133 为我们说明了哪些字节码会出现内存屏障。

  • 普通的读类似 getfield 、getstatic 、 不加 volatile 修饰的数组 load 。
  • 普通的写类似 putfield 、 putstatic 、 不加 volatile 修饰的数组 store 。
  • volatile 读是可以被多个线程访问修饰的 getfield、 getstatic 字段。
  • volatile 写是可以被当个线程访问修饰的 putfield、 putstatic 字段。

这也就是说,只要是普通的读写加上了 volatile 关键字之后,就是 volatile 读写(呃呃呃,我好像说了一句废话),并没有其他特殊的 volatile 独有的指令。

根据这段描述,我们来继续分析一下上面的字节码。

a、b 是全局变量,也就是实例变量,不加 volatile 修饰,u、v 是 volatile 修饰的全局变量;i、j 是局部变量。

首先 i = a、j = b 只是把全局变量的值赋给了局部变量,由于是获取对象引用的操作,所以是字节码指令是 getfield 。

从官方手册就可以知晓原因了。

微信图片_20220416173936.png

地址在 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html

由内存屏障的表格可知,第一个操作是普通读写的情况下,只有第二个操作是 volatile 写才会设置内存屏障。

继续向下分析,遇到了 i = v,这个是把 volatile 变量赋值给局部变量,是一种 volatile 读,同样的 j = u 也是一种 volatile 读,所以这两个操作之间会设置 LoadLoad 屏障。

下面遇到了 a = i ,这是为全局变量赋值操作,所以其对应的字节码是 putfield

微信图片_20220416173940.png

地址在 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html

所以在 j = u 和 a = i 之间会增加 LoadStore 屏障。然后 a = i 和 b = j 是两个普通写,所以这两个操作之间不需要有内存屏障。

继续往下面分析,第一个操作是 b = j ,第二个操作是 v = i 也就是 volatile 写,所以需要有 StoreStore 屏障;同样的,v = i 和 u = j 之间也需要有 StoreStore 屏障。

第一个操作是 u = j 和 第二个操作 i = u volatile 读之间需要 StoreLoad 屏障。

最后一点需要注意下,因为最后两个操作是普通读和普通写,所以最后需要插入两个内存屏障,防止 volatile 读和普通读/写重排序。

《Java 并发编程艺术》里面也提到了这个关键点。

微信图片_20220416173944.png

从上面的分析可知,volatile 实现有序性是通过内存屏障来实现的。

关键概念

在 volatile 实现可见性和有序性的过程中,有一些关键概念,cxuan 这里重新给读者朋友们唠叨下。

  • 缓冲行:英文概念是 cache line,它是缓存中可以分配的最小存储单位。因为数据在内存中不是以独立的项进行存储的,而是以临近 64 字节的方式进行存储。
  • 缓存行填充:cache line fill,当 CPU 把内存的数据载入缓存时,会把临近的共 64 字节的数据一同放入同一个 Cache line,因为局部性原理:临近的数据在将来被访问的可能性大。
  • 缓存命中:cache hit,当 CPU 从内存地址中提取数据进行缓存行填充时,发现提取的位置仍然是上次访问的位置,此时 CPU 会选择从缓存中读取操作数,而不是从内存中取。
  • 写命中:write hit ,当处理器打算将操作数写回到内存时,首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器会将这个操作数写回到缓存,而不是写回到内存,这种方式被称为写命中。
  • 内存屏障:memory barriers,是一组硬件指令,是 volatile 实现有序性的基础。
  • 原子操作:atomic operations,是一组不可中断的一个或者一组操作。
相关文章
|
6月前
|
缓存 Java 编译器
volatile与synchronized
volatile与synchronized
60 0
|
缓存 Java 编译器
|
Java
浅谈volatile
浅谈volatile
70 0
|
SQL
volatile的正确使用(七)
volatile的正确使用(七)
106 0
volatile的正确使用(七)
|
SQL 存储 算法
volatile详解
在单线程环境中,我们几乎用不到这个关键词,但是多线程环境中,这个关键词随处可见。而且也是面试的常客。总的来说,volatile有以下三个特性: 保证可见性; 不保证原子性; 禁止指令重排。 下面就来详细的说说这三个特性。
volatile详解
|
存储 缓存 安全
synchronized&volatile (二)
synchronized&volatile (二)
203 0
|
存储 SQL Java
理解JMM和Volatile
理解JMM和Volatile
144 0
|
缓存 安全 Java
volatile(二)
volatile 这个关键字大家都不陌生,这个关键字一般通常用于并发编程中,是 Java 虚拟机提供的轻量化同步机制,你可能知道 volatile 是干啥的,但是你未必能够清晰明了的知道 volatile 的实现机制,以及 volatile 解决了什么问题,这篇文章我就来带大家解析一波。
volatile(二)
|
存储 缓存 安全
volatile(一)
volatile 这个关键字大家都不陌生,这个关键字一般通常用于并发编程中,是 Java 虚拟机提供的轻量化同步机制,你可能知道 volatile 是干啥的,但是你未必能够清晰明了的知道 volatile 的实现机制,以及 volatile 解决了什么问题,这篇文章我就来带大家解析一波。
volatile(一)
|
缓存 安全 编译器
Volatile
JUC系列
1118 0
Volatile