重排序

#计算机很复杂,下面讲的也只是一些你概念的东西。讲的并不深,如果想完全得弄明白整个计算机跟JVM结合以及程序执行的关系,大家可以在网上继续深究。

1.什么是重排序?

尽管现在为了尽快提升系统的运行效率而在CPU的前面加了多级缓存来减少CPU等待数据存取的时间。但是依然无法充分的运行CPU快速的运算能力。当我们在写java语言的代码的时候,首先编译器会在编译的时候进行一次重排序,然后处理器在执行的时候CPU还会对汇编后的指令集再次进行重排序,所以我们的代码经过两次重排序之后执行的时候的顺序和我们书写的顺序就会出现差异。

2.重排序的可以为所欲为吗

肯定是不能的,不然我们的程序被优化后执行的乱七八糟还有什么意义呢。
重排序会遵守一个叫as-if-serial的规则。就是重排序只会对两个没有数据依赖关系的指令进行重排序,要保证执行后的结果不会被改变。这是在一个线程内才会有效的。多线程的时候就不存在线程之间的指令重排序了。
下面的代码1可能会被重排序,代码2不会。

//有数据依赖关系
int a= 5;
b=a+1;
//没有数据依赖关系
Long  a = new Long(1024*1024*1024L);
 boolean b= true;

注意,这里依然要遵守happens-before原则,但是编译器允许这种不改变最终执行结果的指令重排序,即使他违背了happens-before原则。

3.指令重排序在CPU里面是怎么发生的

看下面这个图
在这里插入图片描述
代码如下

int a = 5;  #1
int b= 10;  #2
int c =a*b; #3

单线程的情况下举例,假设这个线程在CPU1运行。程序里面可以看出3和1、2都有数据依赖关系,所以3一定是最后执行的。只考虑1和2。线程要在slot1里面写入5,然后发现slot1是busy状态,就执行下一步把10写入slot2。之后再执行第三步读取a的时候发现a还没有写入。就再去检查slot1,发现是free状态,就把值写进去。其实指令重排序主要就是为了尽量使用CPU的计算能力。提高CPU的执行效率。
#有可能会发生的乱序执行情况
下面一段代码

int a=100*100; #1
if(a==10000){  #2
int time = 20*3600;#3
}

正常来说,由于存在依赖关系和流程控制关系,那么程序肯定是需要1-2-3的顺序执行的,但是处理器还会对上面的代码进行优化。他会在计算a的值得同时计算出time的值存储在一个叫做(re-order buffer, ROB)重排序缓冲区的地方,如果if满足条件,那么直接把ROB里面的值返回给time。(同时并不是真的同时,只是CPU的运行速度太快,就像同时一样)。

4.多线程下的JMM(Java内存模型)和CPU的内存模型的关系

JMM我之前的博客里面讲过,https://blog.csdn.net/qq_30055391/article/details/84797870 大家可以先看一下。多线程的情况下每个线程都有自己的工作内存,就是自己的栈和程序计数器。注意这个工作内存只是一个抽象的概念,它只是把寄存器和高速缓存抽象在一起而已,他不是一个真实存在的区域,注意它既不是在缓存里面也不是在主内存里面,他只是一个抽象出来的概念。每个线程工作的时候会先把变量使用read和write指令把变量写到自己工作内存的副本里面,然后以后的操作都是在自己的工作内存里面操作的,线程结束后才会把修改后的值通过store和write指令写入到主内存里面。注意它是会主动写的,只是要等待线程结束后才可以。但是每个线程里面会执行很多指令,CPU会通过useage和assign去操作这个值(这个操作是原子)并且把这个值赋值到你的工作内存里面。注意CPU不会完整的执行你所有的指令后才会执行其它线程的命令。这就会出现你修改完了这个值之后还没有写入到主内存的时候,CPU去另一个线程执行对这个值(这个值是从内存里面读取的原始值)的其他操作了,这就是共享变量在多线程下不安全的原因。

5.多线程下的指令重排序造成的恶果

单线程情况下指令重排序会遵从as-if-seria和happens-before规则来保证我们的程序无误的运行。但是多线程的情况下,如果我们不使用volatile synchronize 这些关键字以及加锁的情况下,那么指令重排序会让你头大。

public class InstructionReorder {

    int a;
    boolean finish;

    public void write(){
        a=100*100;    //1
        finish = true;  //2
    }

    public  void read(){
     if( finish==true ){   //3
         int result = a+2000;  //4
         System.out.println("读取完毕");
       }
    }
}

假设有两个线程,线程1执行write方法,线程2执行read方法。我们以序号代表指令。
首先1和2没有数据依赖关系,可以重排序,那么线程2就有可能读到finish=true a=0这样的情况,这就会造成最后的result是不准确的。
第二点:3和4只是存在流程控制关系,CPU可能会采取试探的方式执行,就是先执行result=a+200这条指令,然后把result的值写在重排序缓冲区里面,等finish变为true的时候直接把缓冲区里面的值赋值给result,这样就可能出现线程2还没有接收到finish=true的时候就会预先把result的值计算出来,这样的结果也是不准的。报废了!那我们该怎么办呢?

6.禁止重排序

禁止重排序也要在编译器和处理器两个方面来考虑。
禁止编译器的重排序需要使用优化屏障(Optimization Barrier)来完成。
优化屏障告诉编译器两件事:
1.你别乱动我代码的顺序,我咋写的,你就咋编译,别搞事。
2.你要读取一个值,别在自己工作内存读,去主内存读。
禁止处理器指令重排序需要使用内存屏障(Memory Barrier)来完成。
内存屏障起到两个作用
1.保证数据的可见性
2.防止指令执行的时候CPU对其重排序

7.内存屏障的分类

完全内存屏障(full memory barrier)保障了早于屏障的内存读写操作的结果提交到内存之后,再执行晚于屏障的读写操作。
内存读屏障(Load memory barrier)仅确保了内存读操作;
内存写屏障(store memory barrier)仅保证了内存写操作。
两个组合又可以产生下面的四种
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见(转自 https://www.jianshu.com/p/2ab5e3d7e510最后一小段)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值