有序性
有序性 (Ordering) 指的是在什么情况下一个处理器上运行的一个线程所执行的内存访问操作在另一个处理器上运行的线程看来都是乱序的(Out of Order)。
导致乱序的原因有:指令的重排序和存储子系统的重排序。分别来自编译器处理器和高速缓存写缓冲器
指令重排序
- JIT 编译器的重排序
如果我们在多线程情况下使用这样的语句
helper = new Helper( data );
这个语句可以分解为以下三步伪操作:
1. objRef = allocate(Helper.class);//分配所需内存空间,并获得一个指向该空间的引用
2. invokeConstuctor(objRef);//调用构造器初始化objRef 引用并指向引用的Helpe实例
3. helper = objRef;//将Helper实例引用赋值给实例变量helper
多线程环境下可能出现的情况是3指令被重排到2之前这样就导致 objRef 尚未初始化就赋值给了 helper ,使得helper出现null 的情况
- 处理器重排序
处理器采用了一种叫做猜测执行(Speculation)的技术,这个技术好比是在遇到岔路的情况下,不知道走哪一条,先猜测性地走其中一条如果不通再折返选择另外一条。
package cn.likent.MultiThread;
/**
* @author: kent
* @date: 2018/4/19 10:16
*/
public class SpeculativeLoadExample {
private boolean ready = false;
private int[] data = new int[] {1,2,3,4,5,6,7,8};
public void writer(){
int[] newData = new int[]{1,2,3,4,5,6,7,8};
for (int i = 0; i < newData.length; i++) {
newData[i] = newData[i] - i;
}
data = newData;
ready = true;
}
public int reader(){
int sum = 0 ;
int[] snapshot;
if(ready){
snapshot = data;
for (int i = 0; i < snapshot.length; i++) {
sum += snapshot[i];//多线程的情况下,程序可能直接读取data然后计算sum,临时存放到ROB(ReOrder Buffer)中,再去读取ready的值如果ready为true 就将ROB写到主内存中,反之丢弃计算的 sum
}
}
return sum;
}
}
这个思想和CAS十分相似,总是乐观地操作,根据最终的判断决定是保存还是丢弃计算结果。但是猜测执行可能导致非预期的结果。
存储子系统重排序
类型 | 含义 |
---|---|
LoadLoad | 多个处理器的两个内存读操作的顺序可能不同,如两个读 操作为先L1后L1,但是其他处理器感知顺序变成了先L2后L1。下同 |
LoadStore | 多个处理器两个内存先读后写的命令顺序可能不同 |
StoreLoad | 多个处理器两个内存先写后读的命令顺序可能不同 |
StoreStore | 多个处理器的两个内存写操作的顺序可能不同 |
貌似串行语义
类型 | 代码示例 | 说明 |
---|---|---|
写后读(WAR) | x = 1; y = x+1 | 后一条语句包含前一条语句的执行结果 |
读后写(RAW) | y = x ; x = 1 | 前一条语句读取变量之后,后一条语句更新了该变量的值 |
写后写(WAW) | x = 1;x = 2 | 两条语句对同一变量进行写操作 |
示例:
float price = 59.0f;
short quanity = 5 ;//这两句可以重排序,但是下一句依赖与这两句的执行结果,执行一定是在这两句之后
float subTotal = price * quantity;
如何保证内存访问的顺序性
可以使用 volatile 和 synchronized 关键字禁止重排序,实现有序性