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防止请求中带命令 java防止指令重排_编译器

        

        再来举个更常见的例子:

        考试,在考试的时候老师会告诉我们,先做会做的,不会做的放到后面做。假设出题老师出题顺序是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防止请求中带命令 java防止指令重排_java防止请求中带命令_02

        

        执行顺序:

        源码编译器优化重排序(第一次排序) 指令重排序(第二次)内存重排序(第三次) 最终指向的指令。

        无论是第一次编译器的重排序还是第二、三次的处理器重排序。这些重排序当在多线程的场景下可能会出现线程可见性的问题。

        如在多线程的情况下,单例模式就不安全了。

        为了解决这个问题,JMM允许编译器在生成指令顺序的时候,可以插入特定类型的内存屏障来禁止指令重排序。

        当一个变量使用volatile修饰的时候,volatile关键字就是内存屏障。当编译器在生成指令顺序的时候,发现了volatile,就直接忽略掉。不再重排序了。

        示意图:

        

java防止请求中带命令 java防止指令重排_重排序_03

        

      x