多线程知识梳理(1):当我们谈到指令乱序的时候,在谈什么?

多线程知识梳理(1):当我们谈到指令乱序的时候,在谈什么?

结论

我喜欢先说结论。

程序里面的每行代码的执行顺序,有可能被编译器和CPU根据某种策略给打乱掉。目的是为了性能的提升,让指令的执行能尽可能的并行起来。

可能发生乱序的地方

在Java代码运行过程中,有三处地方会发生指令乱序。

  1. 代码编译过程中,无论是javac将.java文件编译为.class文件的过程中,还是JIT动态编译的过程中,代码的执行顺序都有可能和你当时写的顺序不一样。

  2. CPU执行过程中,CPU在执行指令的时候,并不一定会按照收到的指令顺序去执行,CPU为了让指令尽可能的并行执行,会打乱执行顺序。

  3. 内存乱序,也就是说,CPU的多核之间的指令顺序也不一致。比如CPU0执行的L0(假设这是一个读取操作,执行序号是0),再执行W1(假设这是一个写操作,执行序号是1),但是从另一个CPU1看起来,他可能先看到的是W1,再看到L0.

乱序执行 vs 顺序提交

就像前面收的CPU为了使指令尽可能的并行起来,发明了流水线技术。但如果前后两个指令存在依赖关系,那么后一条语句就要等前一条完成后才能开始。

这里就要说到一个重要的原则:happens before原则

happens before原则

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

  2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

下面是happens-before原则规则:

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

CPU的乱序

CPU为了提高流水线的运行效率,就会做出比如:

  1. 对无依赖的前后指令做出适当的乱序和调度
  2. 对控制以来的指令做分支预测
  3. 对读取内存等耗时操作,做提前预读等

重排序处理器

但是,这里有一个地方非常重要,CPU虽然会按照自己的流水线去乱序执行给定的指令,但是从CPU对外表现来看却不是这样的。这得益于“重排序处理器”。

重排序处理器,会把各个指令的结果按照CPU接收到的指令顺序写入到store buffer、高速缓存或者内存区中

内存乱序

写缓冲区乱序

写缓冲器位于cpu核和高速缓存之间,对X86的架构来说,写缓冲器是FIFO(先进先出)的,因此并不会出现乱序的情况,但是对ARM\Power架构来说,写缓冲区并不能保证FIFO,因此可能会乱序。

高速缓存和写缓冲器的指令重排

cpu会将数据写入写缓冲器的过程是store,从高速缓存或者内存中读取数据是load。

写缓冲器和高速缓存执行store和load的过程都是按照处理器指示的顺序来的,处理的重拍处理器就是按程序的顺序来load和store的,但是其它的处理器看到的可能出现load和store是重排序的,也就是内存重排序。

内存重排的四种可能性:

  1. load load:cpu0先执行l0再执行l1,cpu1看到的是先l1再l0
  2. store store : cpu0限制性w0再执行w1,cpu1看到的是先w1再w0
  3. load store:cpu0先执行l0再执行w1,cpu1看到的是先w1再l0
  4. store load:cpu0限制性w0再执行l1,cpu1看到的是先l1再w0

这样的指令重排可能出现什么样的问题呢?我们简单举个例子:

共享变量

Resource resource = null;

Boolean flag = false;

CPU0

cpu0执行了

resource = loadResource();

flag = true;

CPU1

cpu1执行的代码块是

while(!flag){

​ // 一大堆业务逻辑

​ // 等待信息等等

}

resource.excute();

但是由于内存重排导致,cpu1先看到了cpu0的写操作,也就是flag=true,这个时候代码块就跳出的while循环,开始执行resource.excute()方法。但是CPU1这个时候可能还没有看到cpu0的读取操作也就是resource=loadResource()方法,这个时候cpu1中的resource还是null,那再执行resource.execute()就会出现NPE。

posted @ 2020-04-23 19:17  joimages  阅读(390)  评论(0编辑  收藏  举报