CPU 的乱序执行

CPU 的乱序执行

为什么要乱序执行

  • CPU 的乱序执行本质上是为了提升效率,比如有这样两行命令
    int a = new OtherClass().method();
    int b = 0
    
  • 在这种情况下,a 的结果可能需要很长时间才可以返回,而 b 的值则可以直接得出,同时 b 的值又不依赖于 a ,在这种情况下 CPU 就会乱序执行,这其实是为了提升效率
  • 比如有如下的场景
    洗水壶 > 烧水 > 洗茶壶 > 洗茶杯 > 拿出茶叶 > 泡茶
    //但是我们可以通过合理的设计达到如下顺序
    洗水壶 > 烧水 ========================== > 泡茶
     		      洗茶壶 > 洗茶杯 > 拿出茶叶 
    // 这其实就是一种乱序的操作,但是是为了提高效率,CPU 也是如此
    // 技术源于生活,高于生活
    

don’t talk me ,show me your code !

  • 我们来看看下面的代码,
  • 如果都是顺序执行,是不是永远都不会出现 x = 0 && y = 0 的情况 ?
public class cpu {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        boolean flag = true;
        while (flag) {
            i++; x = 0; y = 0; a = 0; b = 0;
            Thread one = new Thread(() ->{
                    a = 1; x = b;
            });
            Thread other = new Thread(() ->{
                    b = 1; y = a;
            });
            one.start(); other.start();
            one.join(); other.join();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if (x == 0 && y == 0) {
                System.err.println(result);
                flag = false;
            }
        }
    }
}

但是技术总是要虐虐人才体现自己的难度的

在这里插入图片描述
经过测试居然出现了 x = 0 && y = 0 的情况,笔者测试了几次,时间都不是很长,这也就证明了 CPU 的乱序是真实存在的,而且不是很难遇到的现象
有兴趣的小伙伴可以看看这篇文章 Memory Reordering Caught in the Act

但是为什么我们写的代码好像从来没出现过乱序的现象呢?

  • 这是因为,乱序执行的前提是:
    • 下面的指令不受上面的指令影响,就是说如果存在逻辑关系,是不会发生乱序的,而如果两个对象之间不互相影响,其实乱序执行最后的结果也是正确的。这就是 as-if-serial
    • as-if-serial : 不管硬件什么顺序,单线程执行的结果不变,看上去像是serial

CPU 执行乱序主要有以下几种

  • 读读乱序(load load): load(a);load(b); -----------> load(b);load(a);
  • 写写乱序(store store):a=1;b=2-------------> b=2;a=1;
  • 写读乱序(store load): a=1;load(b); ------------> load(b);a=1;
  • 读写乱序(load store): load(a);b=2; ------------> b=2;load(a);

如何避免CPU 乱序执行?

使用 volatile

java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。

  • 一句话即, volatile 声明的变量可以保证多线程对这个变量的可见性,它被称为轻量级的 synchronized, 它比synchronized的使用和执行成本会更低,因为它不会引起线程的阻塞从而导致线程上下文的切换和调度。
回顾下happens-before对volatile规则的定义 : volatile变量的写,先发生于后续对这个变量的读.

这句话的含义有两层

  • volatile 的写操作, 需要将线程 本地内存 值 立马刷新到 主内存的 共享变量 中
  • volatile 的读操作, 需要从 主内存 的 共享变量 中读取,更新 本地内存变量 的值
由此引出 volatile 的内存语义:
  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存.
  • 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量,并更新本地内存的值
volatile 的特性
  • 可见性:对一个volatile的变量的读,总是能看到任意线程对这个变量最后的写入
  • 单个读或者写具有原子性:对于单个 volatile 变量的读或者写具有原子性,复合操作不具有.(如i++)
  • 互斥性:同一时刻只允许一个线程对变量进行操作.(互斥锁的特点)

volatile 是怎么实现的呢?

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

volatile 为什么可以禁止重排序?

volatile 禁止指令重排序的一些规则:

  • 当第二个操作是voaltile写时,无论第一个操作是什么,都不能进行重排序
  • 当地一个操作是volatile读时,不管第二个操作是什么,都不能进行重排序
  • 当第一个操作是volatile写时,第二个操作是volatile读时,不能进行重排序

猜你喜欢

转载自blog.csdn.net/qq_36623327/article/details/107622833
今日推荐