美文网首页
【Java并发(四)】--Java内存模型之重排序

【Java并发(四)】--Java内存模型之重排序

作者: 小安的大情调 | 来源:发表于2018-11-13 23:23 被阅读0次

    如未作特殊说明,文章均为原创,转发请注明出处。

    前言
    java程序中,JMM想尽了各种方法来提高其性能,在软硬结合的情况下,是的JAVA程序能够高效且安全的运行。本文叙述的指令重排序则是JAVA编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

    ​ 在保证最终结果不受影响的情况下进行指令的重排序来提高程序运行的效率。

    ​ 指令重排序需要满足以下两个条件:

    1. 在单线程环境下不能改变程序运行的结果;
    2. 存在数据依赖关系的不允许重排序;

    总而言之,提高性能的前提就是不能影响程序的最终的执行结果。于上篇博文而言,在JAVA对一段执行指令没有强调happens-before(数据的依赖性)时,证明该段指令可以进行指令重排序的优化。


    数据的依耐性

    ​ 这里相当于强调一下上篇博文所指出的happens-before规则。

    ​ 如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据的依赖性。数据依赖分为下来3种类型,如下表:

    名称 代码实例 说明
    写完之后读 a = 1 ; b = a ; 写一个变量之后,再读这个变量
    写完之后再写 a = 1 ;a = 2 ; 写一个变量之后,再写这个变量
    读完之后再写 a = b ; b = 1 ; 读一个变量之后,再写这个变量

    这里列举的三种情况,如果发生发生指令重排序,那么结果都会发生变量

    发生指令重排序(a b 都有初始值 0) 结果 正确的结果😊
    如果第一个操作先是b读取了a的值,再执行a = 1; a = 1 ; b = 0 ; a = 1 ; b = 1 ;
    a = 2 执行再 a = 1 之前 a = 1 ; b = 0 ; a = 2 ; b = 0 ;
    b = 1执行在a = b 之前 a = 1 ; b = 1 ; a = 0 ; b = 1;

    从上述对比可以看出,如果两个操作存在数据的依赖性,那么这两个操作如果发生了指令重排序,就会导致最后执行的结果出错。

    ​ 所以很明县,JAVA知道这一点,如果数据存在依赖性的话,编译器和处理器是不会对该操作进行重排序的,一定会遵守数据依赖性。

    ​ 这里所说的数据依赖性仅仅针对于单个处理器中的单个线程中的执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。

    ​ 由此可见:编译器和处理器的重排序发生的唯一前提是不能影响程序的最终执行结果。


    as-if-serial语义

    ​ as-if-serial语义的意思就是:编译器和处理器在提高性能(重排序来提高指令的并行度),单线程中的程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial

    ​ 由此可以看出,as-if-serial是为了上述数据依赖性提出的规则。

    ​ 为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,那么这些操作就可能会被编译器和处理器重排序,从而提高程序的执行效率。具体下面的实例解析

    int a = 1;         // A
    int b = 2;         // B
    int c = a + b;     // C
    

    上面程序在单线程中代表着三个指令。


    指令关系图

    可以很明显的看出来A与C有数据依赖关系 B与C也有数据依赖关系,但是A与B却没有数据依赖关系。

    所以A、B两个指令编译器和处理器可以对齐进行重排序,但是A、B两个操作必须发生在C操作之前,编译器和处理器必须遵守as-if-serial语义。

    用上篇博文来看待上面的代码的话

    A happens-before C

    B happens-before C

    但是A、B两个操作则没有这种关系。


    重排序对多线程的影响

    ​ 由于在单线程中存在as-if-serial语义,重排序不会对程序的执行结果造成影响,那么在多线程情况下呢?

    public class ReorderExample {
        int a = 0;
        boolean flag = false;
    
        /**
         * A 线程执行
         */
        public void writer() {
            a = 1;              // 1
            flag = true;        // 2
        }
    
        /**
         * B 线程执行
         */
        public void reader() {
            if (flag) {         // 3
                int i = a * a;  // 4
            }
        }
    }
    
    

    如果此时有两个线程,A线程访问writer()方法,B线程访问reader()方法,那么如果此时B线程访问到了第4步,那么此时的i的值是什么呢?

    答案:有可能是1,有可能是0;

    因为在writer()方法中1、2并不存在数据依赖性,那么这两个操作有可能被编译器和处理器进行重排序,在B线程读到flag = true时,此时由于发生重排序,A线程先执行的2操作,那么此时B线程时读取不到A线程写入a的数据的。所以这里的得到的结果出乎意料。

    线程A发生重排序
    这种情况是程序 1、2发生重排序,线程B运行时,正好是线程A给flag赋值,还没有给a赋值的时候,所以线程B不会读取到线程A对a写入的数据。
    线程B发生提前预读,重排序
    第二种情况的重排序发生可能发生在B线程上,因为as-if-serial语义中定义的是数据存在依赖关系时,不能进行重排序,但是操作3、4之间时控制依赖关系,当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测(Speculation)执行来客服控制相关性并行度的影响。以上述为例,在线程B执行if语句时,编译器和处理器可以提前读取并计算a * a并且吧计算记过保存到一个名为重排序缓冲(Reorder Buffer,ROB)的硬件缓冲中。当操作3条件判断为真时,就会把该缓冲的计算结果写入到i中。

    ​ 从上图可以看出,猜测执行会影响多线程的程序语义。

    ​ 在但线程中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。


    总结

    ​ 在多线程并发执行的程序中,在但线程中为了提高程序的并行性的重排序,在多线程的情况下,可能会破坏多线程的执行语义。

    相关文章

      网友评论

          本文标题:【Java并发(四)】--Java内存模型之重排序

          本文链接:https://www.haomeiwen.com/subject/uobrfqtx.html