【Java 并发】【四】volatile的使用以及内存屏障

1  前言

volatile是java语言提供的一个关键字,用来修饰变量的,使用volatile修饰的变量可以保证并发安全的可见性和有序性。

volatile int i = 0;
public void add() {
    i++;
}

使用方法就是声明变量之前加一个volatile关键字,然后变量 i 的操作就跟我们平常的操作是一样的。

但是添加的volatile的变量,在编译之后JVM会在操作该变量的前后添加一些指令来保证可见性和有序性,具体添加了啥指令我们接下来看看。

2  volatile对可见性保证

结合之前讲的MESI缓存一致性协议,看了一些资料。大概知道volatile在JMM层次和CPU高速缓存层次是怎么确保可见性的了:使用volatile关键字修饰的共享变量,每次线程使用之前都会重新从主内存中重新读取最新的值;一旦该共享变量的值被修改了,修改它的线程比如立刻将修改后的值强制刷新回主内存。

(1)首先看一下上面的图,有工作线程A、工作线程B;假如之前工作线程A、B都是用过这个共享变量i,工作内存中都有变量副本 x = 0
(2)这个时候工作线程A要执行 x++ 操作,按照volatile关键字的特性,每次使用之前必须从主内存重新读取,所以工作线程A重新从主内存读取(执行read、load指令)得到 x = 0没有变化
(3)然后执行use指令将 x = 0 传递给工作线程,执行 x++ 操作,得到 x = 1
(4)然后执行assign指令,将 x = 1的结果赋值给工作线程,按照volatile的特性;一旦共享变量的值被修改了,需要立即强制刷新回主内存。所以在执行assign赋值更新后的之后,立马执行store、write指令将最新的值传递到主内存,并且赋值给主内存的变量。
(5)此时工作线程B需要用到共享变量 x 了,即使工作内存里面有副本,但是每次还是会重新从主内存中重新读取最新的值,这个时候读取到 x = 1了

之前我们了解过MESI一致性怎么实现可见性了,由于java内存模型建立在CPU多级缓存模型之上,所以java内存模型底层也是通过MESI一致性的协议去达到可见性的目的,java内存模型会去适配不同操作系统对MESI一致性协议的不同实现方式。

对volatile的了解就到这里了,那么大家可能有这样的疑问:
比如volatile修饰的变量,怎么让每都让它从主存读取最新数据的?
修改了之后立刻刷新会主内存这个是怎么实现的?
还有volatile我看了一些资料说是通过禁止重排序来实现有序性的,那到底是通过什么来禁止重排序的?

其实这个就涉及到一个内存屏障的概念了,其实volatile的可见性和有序性都是通过内存屏障来实现的。包括上面说的读取数据的时候强制读取主内存数据,修改数据之后强制刷新到主内存,都是有相对应的内存屏障指令对应的。还有为了实现有序性而禁止volatile前后相关的指令进行重排序,在JVM乃至操作系统都是有相应的内存屏障指令的。

3  内存屏障

3.1  什么是内存屏障

本质上也是一种指令,只不过它具有屏障的作用而已,无论是在JAVA内存模型还是CPU层次,都是有具体的指令对应的,是一种特殊的指令。这种指令具有屏障的作用,所谓屏障,也就是类似关卡,类似栅栏,具有隔离的作用。

3.2  屏障分类

  • 一类是强制读取主内存,强制刷新主内存的内存屏障,叫做Load屏障和Store屏障
  • 一类是禁止指令重排序的内存屏障,有四个分别叫做LoadLoad屏障、StoreStore屏障、LoadStore屏障、StoreLoad屏障

3.3  强制读取/刷新主内存的屏障

Load屏障:执行读取数据的时候,强制每次都从主内存读取最新的值
Store屏障:每次执行修改数据的时候,强制刷新回主内存。

先画图讲解一下Load屏障:

如上图所示:在工作内存的变量名、变量的值之前有一道关卡或者栅栏,导致变量 i 获取不到工作内存中的值,所以每次只好主内存重新加载咯。

再讲解一下Store屏障:

如上图所示,每次执行assign指令将数据变更之后,后面都会紧紧跟着一个Store屏障,让你立刻刷新到主内存。

也就是说,只要加了Load屏障,相当于加了一个栅栏,不管工作内存是否有数据,都是从主内存读取数据。只要加了Store屏障,具有强制作用,进行assign操作将变量更改了之后,立刻将变量刷新到主内存里面

3.4  禁止指令重排序的屏障

下面再讲讲另外一类的内存屏障,下面这类的内存屏障的作用是禁止指令重排序,JAVA内存模型层次关于禁止重排序有下面4种屏障:

(1)LoadLoad屏障

序列:load1指令  LoadLoad屏障  load2指令
作用:在load1指令和load2指令之间加上 LoadLoad屏障,强制先执行load1指令再执行load2指令;load1指令和load2指令不能进行重排序(也就是说LoadLoad屏障的前面load指令禁止和屏障后面的load指令进行重排序)。

(2)StoreStore屏障

序列:store1指令  StoreStore屏障 store2指令
作用:在store1指令和store2指令之间加上StoreStore屏障,强制先执行store1指令再执行store2指令;store1指令不能和store2指令进行重排序(也就是说StoreStore屏障的前面的store指令禁止和屏障后面的store指令进行重排序)。

(3)LoadStore屏障

序列:load1指令  LoadStore屏障  store2指令
作用:在load1指令和store2指令之前加上LoadStore屏障,强制先执行load1指令再执行store2指令;load1指令和store2执行不能重排序(也就是说LoadStore屏障前面的load执行禁止和屏障后面的store指令进行重排序)。

(4)StoreLoad屏障

序列:store1指令 StoreLoad屏障  load2指令
作用:在store1指令和load2指令之间加上StoreLoad屏障,强制先执行store1指令再执行load2指令;store1指令和load2指令执行不能重排序(这个屏障功能比较强大,StoreLoad屏障前面的Store指令禁止和屏障后面的Store/Load指令进行重排)。

下面画个图,以StoreStore屏障和StoreLoad屏障举个例子,理解下大概是什么意思了:

(1)有三个区域分别是区域1、区域2、区域3
(2)区域1和区域2加了 StoreStore屏障,这样区域1和区域2的Store指令就被隔离开来,不能重排了
(3)区域2和区域3加了StoreLoad屏障,这样区域2和区域3的Store指令、Load指令就被隔离开来,不能重排了
(4)就相当于搞了个栅栏,禁止各个区域之间的指令跳来跳去的,否则就会导致乱序执行

4  小结

在了解完内存屏障以后,下节我们具体看看 volatile 是如何通过内存屏障来保障可见性和有序性的,有理解不对的地方欢迎指正哈。

posted @ 2023-04-02 14:42  酷酷-  阅读(355)  评论(0编辑  收藏  举报