volatile深度解析

volatile的作用:
1.内存可见性
2.禁止指令重排序

可见性是指,在多线程环境下,共享变量的操作对于每个线程来说,都是内存可见的,也就是每个线程获取的volatile变量都是最新值;并且每个线程对volatile变量的修改,都直接刷新到主存。下面重点介绍指令重排序。

一、为什么要指令重排序
为了提高程序执行的性能,编译器和执行器(处理器)通常会对指令做一些优化(重排序):
1.编译器重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
2.处理器重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

第一阶段:编译器优化,发生在编译阶段,就Java而言,就是Java源码编译生成class字节码的时候,对编译生成的中间代码进行一次指令优化。Java的编译器是javac.exe。
第二阶段:执行器(处理器)优化,和不同的处理器硬件厂商的实现有关,也和Java的执行器(java.exe,也称解释器)有关。执行器优化,是对于机器指令在目标平台的机器上运行,做的一层优化。

我们知道,现代高级编程语言,经过编译后,产生目标代码,如.java的源文件编译后生成.class字节码文件,这些编译后的文件,不能直接在机器上运行,而是需要转换成特定平台的机器指令。
在Java中,为了提高运行效率,javac编译器和java解释器,在两个阶段分别对指令进行了优化,也就是重排序。

Java重排序的前提:在不影响单线程运行结果的前提下进行重排序。也就是说,在单线程环境运行,重排序后的结果和重排序之前按代码顺序运行的结果相同。
Java因为指令重排序,优化我们的代码,让程序运行得更快,也随之带来了多线程下,指令执行顺序的不可控。既然指令重排序会影响到多线程执行的正确性,那么我们就需要某些情景下禁止重排序。Java提供给我们禁止重排序能力的的操作就是volatile。

二、JVM的volatile是如何禁止重排序的
happen-before原则保证了程序的“有序性”,它规定,如果两个操作的执行顺序无法从happen-before原则中推导出来,那么它们就不能保证“有序性”,可以随意进行重排序。其定义如下:
1、同一个线程中的,前面的操作 happen-before 后续的操作。(即单线程内按代码顺序执行。但是,在不影响在单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这是合法的。换句话说,这一是规则无法保证编译重排和指令重排)。

2、监视器上的解锁操作 happen-before 其后续的加锁操作。(Synchronized 规则)

3、对volatile变量的写操作 happen-before 后续的读操作。(volatile 规则)

4、线程的start() 方法 happen-before 该线程所有的后续操作。(线程启动规则)

5、线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作。

6、如果 a happen-before b,b happen-before c,则a happen-before c(传递性)。

着重看第三点volatile规则:对volatile变量的写操作 happen-before 后续的读操作。

加入volatile关键字时,会多出一个lock前缀指令。volatile变量在字节码级别没有任何区别,在汇编级别使用了lock指令前缀。
简单理解也就是说,lock后就是一个原子操作。原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。

当使用lock指令前缀时,它会使CPU宣告一个LOCK#信号,这样就能确保在多处理器系统或多线程竞争的环境下互斥地使用这个内存地址。当指令执行完毕,这个锁定动作也就会消失。

感觉有点像Java的synchronized锁,但volatile底层使用多核处理器实现的lock指令,更底层,消耗代价更小。

因此有人将Java的synchronized看作重量级的锁,而volatile看作轻量级的synchronized,并不是全无道理。

本质:
lock前缀指令其实就相当于一个内存屏障。内存屏障是一组CPU处理指令,用来实现对内存操作的顺序限制。volatile的底层就是通过内存屏障来实现的。



编译器和执行器可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。正如去西藏途中各个站点的先后顺序在你心中都一清二楚。

内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值。这正是volatile实现内存可见性的基础。

三、小结
1、Java重排序的前提:在不影响单线程运行结果的前提下进行重排序。也就是在单线程环境运行,重排序后的结果和重排序之前按代码顺序运行的结果相同。

2、指令重排序对单线程没有什么影响,它不会影响程序的运行结果,反而会优化执行性能,但会影响多线程的正确性。

3、Java因为指令重排序,优化我们的代码,让程序运行更快,也随之带来了多线程下,指令执行顺序的不可控。

4、volatile的底层是通过lock前缀指令、内存屏障来实现的。


猜你喜欢

转载自www.cnblogs.com/yuanfei1110111/p/10327390.html
今日推荐