深入理解java内存模型02-重排序

深入理解java内存模型 -学习笔记
深入理解java虚拟机
JSR133
转载自并发编程网 本文链接地址: 深入理解Java内存模型

硬件的效率一致性


高速缓存 & 缓存一致性
由于计算机的存储设备和处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器的高速缓存(Cache)来作为内存与处理器之间的缓冲;将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后在从缓存同步回内存中,这样处理器就无需等待缓慢的内存读写了。

基于高速缓存的存储交互很好的解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一内存(Main Memory),
这里写图片描述
当多处理器的运算任务都涉及同一块主内存区域时,可能会导致各自的缓冲数据不一致问题。为了解决一致性问题,需要各个处理器访问缓存时都遵循一些协议,这些协议有:MSI,MESI,MOSI,Filefly及Dragon Protocol.
除了增加高速缓存之外,为了使处理器内部的运算元能尽可能被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算机将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的(但并不保证程序中各个语句计算的先后顺序与输入代码的顺序一致)。


重排序种类

在执行的过程中,编译器和处理器常常会对指令做重排序。重排序分3中类型:

  1. 编译器优化的重排序:编译器在不改变单线程程序语义下的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序:现代处理器采用了指令级并行技术(Instraction-Level Parallelism,ISP)将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从java源码到最终执行的指令序列,会分别进行下面的三种重排序
这里写图片描述
这些排序都可能会导致多线程程序出现内存可见性问题。

  • 对于编译器重排序:JMM的编译器重排序规则会禁止特定类型的编译器重排序(not all)
  • 对于处理器重排序:JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定的内存屏障(memory barriers)指令,通过内存屏障指令来禁止特定类型的处理器重排序。

重排序


处理器重排序与内存屏障指令
现代的处理器使用写缓冲区来临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,可以减少对内存总线的占用。虽然写缓冲区有这么多好处,但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致!

从单个线程的角度看,只要重排序不会影响到该线程的执行结果,编译器就可以对该线程中的指令进行重排序。

为了具体说明请看下例:
这里写图片描述
产生如上结果的原因可能如下图:
这里写图片描述
这里处理器A和处理器B可以同时把共享变量写入自己的写缓冲区(A1,B1),然后从内存中读取另一个共享变量(A2,B2),最后才把自己写缓存区中保存的脏数据刷新到内存中(A3,B3)。当以这种时序执行时,程序就可以得到x = y = 0的结果。

虽然处理器A执行内存操作的顺序为:A1->A2,但内存操作实际发生的顺序却是:A2->A1。此时,处理器A的内存操作顺序被重排序了。


处理器重排序类型
这里写图片描述

从上表我们可以看出:

  • 常见的处理器都允许Store-Load重排序;
  • 常见的处理器都不允许对存在数据依赖的操作做重排序。
  • sparc-TSO和x86拥有相对较强的处理器内存模型,它们仅允许对写-读操作做重排序(因为它们都使用了写缓冲区)。

为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM把内存屏障指令分为下列四类:

屏障类型指令示例说明
LoadLoad BarriersLoad1;
LoadLoad;
Load2
确保Load1数据的装载,之前于Load2及所有后续装载指令的装载。
StoreStore BarriersStore1;
StoreStore;
Store2
确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储。
LoadStore BarriersLoad1;
LoadStore;
Store2
确保Load1数据装载,之前于Store2及所有后续的存储指令刷新到内存。
StoreLoad BarriersStore1;
StoreLoad;
Load2
确保Store1数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。

happens-before
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。

  • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个监视器锁的unlock(),happens- before 于随后对这个监视器锁的lock()。
  • volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
  • 在某个线程对象上调用 start()方法 happens-before 该启动了的线程中的任意动作。
  • 某个线程中的所有动作 happens-before 任意其它线程成功从该线程对象上的join()中返回。
  • 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。

两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见。


数据依赖性
若两个操作访问同一个变量,且这两个操作中至少有一个为写操作,此时这两个操作就存在数据依赖性。

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

上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会改变。
编译器和处理器在重排序时,会遵守数据依赖性,它们不会改变存在数据依赖关系的两个操作的执行顺序。
这里说的数据依赖性仅针对单个处理器执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器参考。


as-if-serial
as-if-serial:不管怎么重排序,单线程程序执行的结果不能被改变。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。
为了具体说明,请看下面计算圆面积的代码示例:

double pi  = 3.14;    //A
double r   = 1.0;     //B
double area = pi * r * r; //C

这里写图片描述

as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。


程序顺序规则
根据happens-before的程序顺序规则,上面计算圆的面积的示例代码存在三个happens- before关系:

  • A happens-before B
  • B happens-before C
  • A happends-before C :根据happens- before的传递性推导出来的。

如果A happens- before B,JMM并不要求A一定要在B之前执行。JMM仅仅要求前一个操作(执行的结果)对后一个操作可见。
而且重排序操作A和操作B后的执行结果,与操作A和操作B按happens- before顺序执行的结果一致。在这种情况下,JMM会认为这种重排序并不非法(not illegal),JMM允许这种重排序。

重排序对多线程的影响

public class ReorderExample {
    int a = 0;
    boolean flag = false;

    public void writer() {
        a = 1;                   //1
        flag = true;             //2
    }

    public void reader() {
        if (flag) {                //3
            int i =  a * a;        //4
            System.out.println("i="+i);//打印 i = 0
        }

    }
}

这里假设有两个线程A和B,A首先执行writer()方法,随后B线程接着执行reader()方法。
由于线程A内可能发生重排序,由于在A线程内1和2没有数据依赖性,可能会重排序顺序 2 -> 1,
这就导致B线程,打印结果i = 0;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值