3.缓存一致性-指令重排-内存屏障

1. 缓存一致性

为了保证内存可见性,Java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM 把内存屏障指令分为4类
在计算机中,cpu和内存的交互最为频繁,相比内存,磁盘读写太慢,内存相当于高速的缓冲区。

但是随着cpu的发展,内存的读写速度也远远赶不上cpu。因此cpu厂商在每颗cpu上加上高速缓存,用于缓解这种情况

image
cpu上加入了高速缓存这样做解决了处理器和内存的矛盾(一快一慢),但是引来的新的问题 - 缓存一致性

在多核cpu中,每个处理器都有各自的高速缓存(L1,L2,L3),而主内存确只有一个

CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找,每个cpu有且只有一套自己的缓存

2. 指令重排

指令重排是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

2.1. 重排的类型

指令重排的类型分3种:

  1. 编译器优化的重排
    编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
  2. 指令级并行的重排
    现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
  3. 内存系统的重排
    由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行

1属于编译器重排序,2和3属于处理器重排序

2.2. 数据依赖性

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性

简单来说,下一步的数据操作依赖上一步操作的数据,即为数据依赖性

编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序

2.3. as-if-serial 语义

as-if-serial 语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。

编译器、runtime 和处理器都必须遵守 as-if-serial 语义。

为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序

double pi  = 3.14;           // A
double r   = 1.0;            // B
double area = pi * r * r;    // C
double r   = 1.0;            // B
double pi  = 3.14;           // A
double area = pi * r * r;    // C

上面两段代码执行结果相同,AB可以重新排序,但是C不可以,因为C依赖AB,他一定会在AB执行完成之后才执行。

2.4. 程序顺序规则happens-before

根据 happens-before 的程序顺序规则,2.3 的示例代码存在3个 happens-before 关系。

1)A happens-before B。

2)B happens-before C。

3)A happens-before C。

这里 A happens-before B,但实际执行时 B 却可以排在 A 之前执行(看上面的重排序后的执行顺序)。如果 A happens-before B,JMM 并不要求 A 一定要在 B 之前执行。

JMM 仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。

这里操作 A 的执行结果不需要对操作 B 可见;而且重排序操作 A 和操作 B 后的执行结果,与操作 A 和操作 B 按 happens-before 顺序执行的结果一致。

在这种情况下,JMM 会认为这种重排序并不非法(not illegal),JMM 允许这种重排序。

2.5. 指令重排对多线程的影响

class ReorderExample {
       int a = 0;
       boolean flag = false;
       public void writer() {
           a = 1;                  // 1
           flag = true;            // 2
       }
       Public void reader() {
           if (flag) {            // 3
               int i =  a * a;     // 4
               ……
           }
       }
}

由于操作1和操作2 操作3和操作4 没有数据依赖关系,编译器和处理器可以对这两个操作进行指令重排

操作3和操作4存在控制依赖关系

当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测(Speculation)执行来克服控制相关性对并行度的影响

以处理器的猜测执行为例,执行线程 B 的处理器可以提前读取并计算 a*a,然后把计算结果临时保存到一个名为重排序缓冲(Reorder Buffer,ROB)的硬件缓存中

当操作3的条件判断为真时,就把该计算结果写入变量 i 中

如果重排序 之后 执行顺序可能是
2 -> 3 -> 4 -> 1 那么 i=0 ,这与我们的预想是不同的
4 -> 1 -> 2 -> 3 在这种情况下 无论1 2 哪个先执行结果都为0

3. 内存屏障

sfance lfance mfance disruptor JMH 9527
为了保证内存可见性,Java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM 把内存屏障指令分为4类

3.1. LoadLoad屏障

指令示例:Load1; LoadLoad; Load2 读取指令;屏障指令;读取指令

  • Load1 和 Load2 代表两条读取指令
  • LoadLoad屏障指令前的所有读取指令都会比之后的读取指令先执行

3.2. StoreStore屏障

指令示例:Store1; StoreStore; Store2 写入指令;屏障指令;写入指令

  • Store1 和 Store2代表两条写入指令
  • StoreStore屏障指令确保屏障前操作的数据对其他处理器可见(刷新到内存)
  • StoreStore屏障指令前的所有写入指令都会比之后的写入指令先执行

3.3. LoadStore屏障

指令示例景:Load1; LoadStore; Store2 读取指令;屏障指令;写入指令

  • Load1 代表读取指令
  • Store2 代表写入指令
  • LoadStore屏障指令确保屏障前的读取指令都比之后的写入指令先执行

3.4. StoreLoad屏障

指令示例:Store1; StoreLoad; Load2 写入指令;屏障指令;读取指令

  • Store1 代表写入指令
  • Load2 代表读取指令
  • StoreLoad屏障指令确保屏障前操作的数据对其他处理器可见(刷新到内存)
  • StoreLoad屏障指令确保屏障前的写入指令都比之后的读取指令先执行

StoreLoad Barriers 是一个“全能型”的屏障,它同时具有其他3个屏障的效果。现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值