Java并发编程之验证volatile指令重排-理论篇

JAVA并发编程之验证volatile指令重排-理论篇
Java并发包下的类中大量使用了volatile关键字 。通过之前文章介绍,大家已经知道了volatile的三大特性:共享变量可见性;不保证原子性;禁止指令重排后顺序性 。通过前面两篇文章我们通过代码验证了前两个特性,本文我们就来验证禁止指令重排保证顺序性 。
指令重排序的生活例子去餐厅吃饭预定位置的的时候 。假设要去A餐厅吃饭,A餐厅有前台B、服务员C以及老板D 。如果就只有你一个人去吃饭的时候,你给前台或者给服务器或者给老板说一声把2号桌预定了,半小时后过来 。餐厅在为了2小时内就你一个人去吃饭 。那么OK,没问题,别说等半个小时,就是等一个小时,2号桌还是你的 。
但是,如果现在是吃饭高峰期,很多人来吃饭,你给前台说了,前台忙着没有及时给服务员或者没有给老板说,这个时候有个路人甲来吃饭,刚好看到2号桌没人,老板或者服务员就让他就坐2号桌吃饭了 。那么,等你过来的时候,2号桌已经有人了 。这个时候对于你来说,这个结果就不是你想要的了 。
上面案例,如果从计算机执行指令角度来分析的话,你要到2号桌吃饭,这是预期结果 。餐厅A就相当于是处理器,前台B就相当于是编译器,服务员C和老板D就是指令和内存系统 。如果你预定的时间点不是吃饭高峰期或者没有人去餐厅A吃饭 。那么你就相当于是一个线程 。就是单线程的 。老板、前台、服务员怎么安排都可以 。因为只有你一个2号桌肯定是你的 。这是单线程情况下 。预期结果与实际结果就是一致的 。
如果你预定的时间点是吃饭高峰期,很多人来吃饭(很多线程),这个时候为了餐厅效益,无论是前台还是服务员或者是老板都会对你的位置进行重排序 。在你没有来的时候,会安排其他人到你预定的位置吃饭 。如果其他人在你的位置吃饭,这个时候你再来吃饭,那么实际结果和预期结果就不一样了 。这个时候餐厅应该做出相应的赔偿 。为了解决这种赔偿问题,老板就想到了一个方案 。做个牌子放在客人预定的桌子上 。
当前台或者是服务员或者是老板看到餐桌上放的这个牌子,就知道这个位置不能再调动了 。其中这个放在餐桌上的牌子就是特殊类型的内存屏障了 。
示意图如下:

Java并发编程之验证volatile指令重排-理论篇

文章插图
 
再来举个更常见的例子:
考试,在考试的时候老师会告诉我们,先做会做的,不会做的放到后面做 。假设出题老师出题顺序是1-5,但是考试会根据自己实际情况做题顺序有可能是1、2、4、5、3或者是1、3、4、5、2等等 。如果把出题老师看着是写代码的程序员,题目的顺序是代码一行一行的顺序,你的老师会告诉你先做会做的,此时老师就相当于是编译器,会排序一次 。然后你自己做的时候又会进行重新排序,你自己就相当于是处理器又排序了一次 。
上面两个现实生活中的案例,我们弄明白后,再来看看在计算机中指令重排问题,就很容易理解了 。
指令重排我们程序员编写的代码在JVM执行的时候,为了提高性能,编译器和处理器都会对代码编译后的指令进行重排序 。分为3种:
1:编译器优化重排:
编译器的优化前提是在保证不改变单线程语义的情况下,对重新安排语句的执行顺序 。
2:指令并行重排:
如果代码中某些语句之间不存在数据依赖,处理器可以改变语句对应机器指令的顺序
如:int x = 10;int y = 5;对于这种x y之间没有数据依赖关系的,机器指令就会进行重新排序 。但是对于:int x = 10; int y = 5; int z = x+y;这种的,因为z和x y之间存在数据依赖(z=x+y)关系 。在这种情况下,机器指令就不会把z排序在xy前面 。
3:内存系统的重排序
通过之前的学习,我们知道了处理器和主内存之间还存在一二三级缓存 。这些读写缓存的存在,使得程序的加载和存取操作,可能是乱序无章的 。
指令重排序的流程图通过上面介绍,我们可以知道从程序员写的Java源码到处理器真正实际执行的指令序列,会经历如下图的过程:
 
Java并发编程之验证volatile指令重排-理论篇

文章插图
 
执行顺序:
源码编译器优化重排序(第一次排序) 指令重排序(第二次)内存重排序(第三次) 最终指向的指令 。
无论是第一次编译器的重排序还是第二、三次的处理器重排序 。这些重排序当在多线程的场景下可能会出现线程可见性的问题 。


推荐阅读