zoukankan      html  css  js  c++  java
  • 多线程知识梳理(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(先进先出)的,因此并不会出现乱序的情况,但是对ARMPower架构来说,写缓冲区并不能保证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。

  • 相关阅读:
    自己常用的数据库操作语句
    我被SQL注入撞了一下腰
    分页
    reset.css
    创建对象的多种方式
    js 数组去重
    学习JS防抖【节流】
    localStorage.js
    vue 项目移动端使用淘宝自适应插件 环境配置
    Vue项目搭建
  • 原文地址:https://www.cnblogs.com/joimages/p/12762938.html
Copyright © 2011-2022 走看看