Java精通并发-volatile与内存屏障的重要语义详细分析

在上一次https://www.cnblogs.com/webor2006/protected/p/12595201.html咱们已经对于volatile关键字的作用进行了一定的了解,这里回顾一下:

 

上一次对于第一条作用进行了详细的解读了,接下来则来解读一下剩下的两条:防止指令重排序、实现变量的可见性。而这俩其实都是通过一种手段来实现的:内存屏障(memory barrier),所以要想搞清楚这这两条,必须得先来理解内存屏障这个概念,所以接下来重点来搞清楚内存屏障这个平常听得比较少的这个概念。

何为指令重排序?

这其实涉及到JIT(Just In Time)的一些功能,在现代化的JVM编译器当中,它会根据我们所写的代码的情况自动的一定程序的优化,其中优化当中就有一个可能就是会对咱们的指令进行一定的修改,比如按照顺序执行了三条指令:1、2、3【对应我们的代码顺序】,但是在编译完之后可能生成的字节码会变成3、2、1,或1、3、2等,也就是对指令进行重排序了,这里用一个简单的例子来直观的看一下指令重排序的大概思想:

int a = 0;
int b = 1;

a++;

重排后可能为:

int a = 0;
a++;
int b = 0;

对于这个重排序其实是编译器为了让我们的程序执行的性能更高而采取的一种优化手段,但是!!!在极端情况下这种指令重排序的优化手段并不是我们需要的,所以此时就需要防止某些指令重排序,而是按我们所编写的代码的顺序来执行。对于指令重排序而言,在单线程环境下肯定是没任何问题的,如果有问题也不可能出现这种优化策略了,重点是在多线程的环境下这种所谓优化的指令重排序策略可能就会产生问题,而这个volatile关键字就具备这种防止指令重排序的功能。

阐述内存屏障(memeory barrier):

volatile写入操作:

这里先来看一个简单代码:

int a = 1;
String s = "Hello";

volatile boolean v = false; //写入操作

此时则会在volatile这句代码之前和之后插入相应的内存屏障:

int a = 1;
String s = "Hello";
内存屏障
volatile boolean v = false; //写入操作
内存屏障

而内存屏障是存在有分类的,这里给内存屏障再细化一下则为:

int a = 1;
String s = "Hello";
内存屏障 (Release Barrier,释放屏障volatile boolean v = false; //写入操作
内存屏障(Store Barrier,存储屏障

很显然这个屏障我们肉眼是看不到的,是借助于volatile来实现的,那对于这俩个屏障对于程序有啥作用呢?下面来解释一下:

  • Release Barrier:防止下面的volatile与上面的所有操作的指令重排序,并可以让在内存屏障之前所发生的读写操作都能立刻的发布到所有的程序当中,其它线程就能立刻看到其修改的结果。啥意思?
  • Store Barrier:它的重要作用是刷新处理器的缓存,结果是可以确保该存储屏障之前一切的操作所生成的结果对于其他处理器来说都可见,也就是:

volatile读取操作:

对于volatile的读操作其内存屏障又是不一样的,下面来看一下:

int a = 1;
String s = "Hello";
内存屏障 (Release Barrier,释放屏障)
volatile boolean v = false; //写入操作
内存屏障(Store Barrier,存储屏障)

boolean v1 = v; //读取操作

int a = 1;
String s = "Hello";

此时由于遇到了volatile的读取操作,则又会产生内存屏障了,它有别于之前看到的写入的屏障,如下:

int a = 1;
String s = "Hello";
内存屏障 (Release Barrier,释放屏障)
volatile boolean v = false; //写入操作
内存屏障(Store Barrier,存储屏障)

内存屏障 (Load Barrier,加载屏障)
boolean v1 = v; //读取操作
内存屏障 (Acquire Barrier,获取屏障)

int a = 1;
String s = "Hello";

那这两种屏障又有何义呢?

  • Load Barrier:可以刷新处理器缓存,同步其他处理器对该volatile变量的修改结果。也就是:
  • Acquire Barrier:可以防止上面的volatile读取操作与下面的所有操作语句的指令重排序。

可以发现:

对于volatile关键字变量的读写操作,本质上都是通过内存屏障来执行的,而内存屏障兼具了如下两方面的能力:

1、防止指令重排序。

2、实现变量内存的可见性。

所以:

最后总结一下:

1、对于读取操作来说,volatile可以确保该操作与其后续的所有读写操作都不会进行指令重排序。

2、对于修改操作来说,valatile可以确保该操作与其上面的所有读写操作都不会进行指令重排序。

注意:

在上面的举例中都是Java的原生数据类型:

如果是一个引用类型呢?比如说ArrayList,那对于volatile的内存屏障功效是不起作用的,为啥?因为ArrayList中的读写操作都不是原子的,比如读操作,得先找到元素的地址,然后再进行读取,但是!!如果将ArrayList的引用赋值给另一个volatile的ArrayList,这就可以确保原子操作,也就有了volatile相关的功效了。

再论volatile和锁【面试题】:

在上一次volatile的学习中已经针对它们俩的相同与不同点做了一个阐述,回忆一下:

这里由于学到了内存屏障的知识点,所以需要再拉出来进一步阐述一下,对于synchronized代码块而言,对应的字节码指令我们都知道会是如下:

monitorenter
.....
monitorexit

我们知道锁的功能比volatile功能更强大,因为它有排他性,对于volatile它不是有指令重排序和内存可见性的功效,那锁有木有呢?当然有,所以加上这个功效之后的锁背后的形态就会变为:

monitorenter
内存屏障 (Acquire Barrier,获取屏障)//刷新处理器缓存,同步其他处理器对该volatile变量的修改结果,也就是获取最新的值
.....
内存屏障 (Release Barrier,释放屏障)//也就是将处理结果发布出去,刷新处理器缓存
monitorexit

以上就是对于同步锁的一个完整的形态。

总结:

1、volatile关键字自身的劣势:它相比不使用volatile的变量而言,性能有损失,因为对于有volatile的变量,则每次都是会从主内存(高速缓存)中来获取了,而如果不使用volatile的变量,则会直接从寄存器上获取,要明白,寄存器要比内存获取快多的,所以这个关键字不要烂用。

2、volatile相比锁,优点是volatile不存在阻塞,也不会进行用户态到内核态的切换,而锁肯定是要阻塞且会进行用户和内核态的切换;缺点是它不具备锁的排它性。

通过这两篇的总结,我觉得就已经能彻底来理清这个关键字的含义了,真的涉及到的概念还是很难理解的。

原文地址:https://www.cnblogs.com/webor2006/p/12598378.html