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

volatile 是怎么实现的呢?

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

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

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

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

  • 当第二个操作是voaltile写时,无论第一个操作是什么,都不能进行重排序
  • 当地一个操作是volatile读时,不管第二个操作是什么,都不能进行重排序
  • 当第一个操作是volatile写时,第二个操作是volatile读时,不能进行重排序
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 内存屏障是一种CPU指令,用于保证内存访问的有性。它可以分为编译屏障和CPU执行屏障。 编译屏障是在代码编译时插入的指令,用于告诉编译器在这个位置需要生成一条内存屏障指令。它可以保证在编译后生成的机器码中,这个位置的内存访问会被正确地列化。 CPU执行屏障是在CPU级别执行的指令,用于告诉CPU在这个位置需要保证内存访问的顺性。在现代CPU中,由于硬件优化和多核并发等原因,CPU会对指令进行执行,而屏障可以保证在这个位置之前的所有内存访问都已经完成,从而避免了执行带来的问题。 总的来说,内存屏障是一种非常重要的机制,可以保证多线程程中的内存访问顺性,避免出现数据竞争等问题。 ### 回答2: 内存屏障是指用于控制内存访问顺的指令或者指令列。根据其功能和作用方式的不同, 内存屏障可以分为编译屏障和CPU执行屏障。 编译屏障是在编译器层面进行优化控制的屏障。编译器在进行程优化时,可能会对代码进行重排,以提高执行效率。然而,有些代码的执行是有严格要求的,此时就需要使用编译屏障来保证指令的顺。编译屏障可以用于控制指令的插入位置,确保指令的执行符合预期。编译屏障通常是通过特殊的指令或者关键字来实现的,例如在C语言中的"__asm__ __volatile__"关键字。 CPU执行屏障是在CPU层面进行指令执行时的控制屏障。现代处理器在执行指令时会使用执行技术,执行可以提高指令级并行度从而提高处理器的性能。然而,在某些情况下,由于指令之间存在依赖关系,需要保证指令的执行,此时就需要使用屏障。屏障可以阻止指令执行的同时也确保了数据的一致性。屏障一般是通过特殊的指令来实现的,例如在x86架构中的"mfence"指令。 总的来说,编译屏障主要是用于控制编译器对代码的优化,保证指令的执行;而CPU执行屏障主要是用于控制CPU对指令的执行,保证指令的执行。两者在不同的层面上起到了优化和控制的作用,都是为了保证程的正确执行和数据的一致性。 ### 回答3: 内存屏障是一种在并发编程中用来确保内存操作有性的机制。内存屏障分为编译屏障和CPU执行屏障两种类型。 编译屏障是在编译器层面上插入的指令,用于告诉编译器在指定位置之前的所有内存访问操作必须完成,并且在指定位置之后的所有内存访问操作必须等待。编译屏障可以通过优化和重排指令来提高程执行效率,但是在多线程环境下可能会导致并发访问数据的顺问题。因此,通过插入编译屏障来限制指令重排,确保内存操作按照预期的顺进行。 CPU执行屏障是在指令执行层面上插入的机制,用于告诉CPU在指定位置之前的所有内存访问操作必须完成,并且在指定位置之后的所有内存访问操作必须等待。CPU执行屏障主要解决CPU执行指令的问题,确保内存操作的顺性。在现代处理器中,由于执行可以提高指令执行效率,但可能导致结果和预期不符。因此,通过插入CPU执行屏障来确保内存操作的有性。 总结起来,编译屏障和CPU执行屏障是为了解决并发编程中的内存操作顺问题而设计的机制。编译屏障在编译器层面上限制指令重排,确保内存操作有进行;CPU执行屏障在指令执行层面上限制指令执行,保证内存操作的有性。这两种屏障在不同的层面上发挥作用,共同保证程的正确执行

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值