JMM内存模型的重排序是指在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序(看到这句话时,我就想到了这样两个问题:为什么重排序能够提高性能呢?会不会破坏程序运行结果?)
重排序分为三种类型:
1. 编译器优化重排序
2. 指令级并行重排序(处理机)
3. 内存系统重排序(处理机)
为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理机排序,JMM把内存屏障指令分成4类:
屏障类型 指令示例 说 明
LoadLoad Barriers Load1:LoadLoad:Load2 Load1数据装载先于Load2
StoreStore Barriers Store1:StoreStore:Strore2 确保Store1数据刷新到内存
LoadStore Barriers Load1:LoadStore:Store2 确保Load1装载先于Store2
StoreLoad Barriers Store1:StoreLoad:Load2 确保Store1数据刷新到内存先于Load2
-->happens-before关系用于阐述操作之间内存的可见性。如果一个操作的执行结果需要对另一个内存可见,那么两个操作必须存在happens-before关系。(这两个操作既可以在一个线程内也可以在不同线程之间)
由此可见,当指令之间存在数据依赖关系时,JMM会插入适当内存屏障来防止重排序,从而保证运行结果的一致性。
数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分下列三种类型:
名称 代码 示例说明
写后读 a = 1;b = a; 写一个变量之后,再读这个位置。
写后写 a = 1;a = 2; 写一个变量之后,再写这个变量。
读后写 a = b;b = 1; 读一个变量之后,再写这个变量。
上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。
-->as-if-serial语义:不管怎么重排序,程序的执行结果不能改变。
重排序对多线程的影响
现在让我们来看看,重排序是否会改变多线程程序的执行结果。请看下面的示例代码:
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
……
}
}
}
flag变量是个标记,用来标识变量a是否已被写入。这里假设有两个线程A和B,A首先执行writer()方法,随后B线程接着执行reader()方法。线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写入?
答案是:不一定能看到。
由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样,操作3和操作4没有数据依赖关系(但操作3和操作4存在控制依赖关系),编译器和处理器也可以对这两个操作重排序。导致线程B不知道变量i是否被写入了而误操作。
总结:在单线程程序中,对存在控制依赖的操作重排序不会改变程序执行的结果(不是很明白),这也是as-if-serial语义允许对存在控制依赖操作做出重排序的原因;但在多线程程序中对控制依赖的操作做重排序,可能改变程序的执行结果。
网友评论