为了保证内存可见性,Java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM 把内存屏障指令分为4类
在计算机中,cpu和内存的交互最为频繁,相比内存,磁盘读写太慢,内存相当于高速的缓冲区。
但是随着cpu的发展,内存的读写速度也远远赶不上cpu。因此cpu厂商在每颗cpu上加上高速缓存,用于缓解这种情况
cpu上加入了高速缓存这样做解决了处理器和内存的矛盾(一快一慢),但是引来的新的问题 - 缓存一致性
在多核cpu中,每个处理器都有各自的高速缓存(L1,L2,L3),而主内存确只有一个
CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找,每个cpu有且只有一套自己的缓存
指令重排是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
指令重排的类型分3种:
1属于编译器重排序,2和3属于处理器重排序
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性
简单来说,下一步的数据操作依赖上一步操作的数据,即为数据依赖性
编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序
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执行完成之后才执行。
根据 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 允许这种重排序。
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
sfance lfance mfance disruptor JMH 9527
为了保证内存可见性,Java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM 把内存屏障指令分为4类
指令示例:Load1; LoadLoad; Load2 读取指令;屏障指令;读取指令
读取指令
都会比之后的读取指令
先执行指令示例:Store1; StoreStore; Store2 写入指令;屏障指令;写入指令
StoreStore屏障指令
确保屏障前操作的数据对其他处理器可见(刷新到内存)StoreStore屏障指令
前的所有写入指令
都会比之后的写入指令
先执行指令示例景:Load1; LoadStore; Store2 读取指令;屏障指令;写入指令
LoadStore屏障指令
确保屏障前的读取指令
都比之后的写入指令
先执行指令示例:Store1; StoreLoad; Load2 写入指令;屏障指令;读取指令
StoreLoad屏障指令
确保屏障前操作的数据对其他处理器可见(刷新到内存)StoreLoad屏障指令
确保屏障前的写入指令
都比之后的读取指令
先执行StoreLoad Barriers 是一个“全能型”的屏障,它同时具有其他3个屏障的效果。现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)