JVM内存模型之Happens-Before和重排序

Happens-Before

从JDK5开始,java使用新的JSR-133内存模型(文件较大,打开可能会比较缓慢) JSR-133使用happens-before的概念来阐述操作之间的内存可见性.

Happens-Before Relationship : 先行发生关系,如果有两个操作A和B存在A Happens-Before B,那么操作A对变量的修改对操作B来说是可见的。这个先行并不是代码执行时间上的先后关系,而是保证执行结果是顺序的。当程序包含两个没有被Happens-Before关系排序的冲突访问时,就称存在数据争用。

先行发生关系的规则

  • 在同一个线程里面,按照代码执行的顺序(也就是代码语义的顺序),前一个操作先于后面一个操作发生
  • 对一个monitor对象的解锁操作先于后续对同一个monitor对象的锁操作
  • 对volatile字段的写操作先于后面的对此字段的读操作
  • 对线程的start操作(调用线程对象的start()方法)先于这个线程的其他任何操作
  • 一个线程中所有的操作先于其他任何线程在此线程上调用 join()方法
  • 如果A操作优先于B,B操作优先于C,那么A操作优先于C

重排序

数据依赖性

如果两个操作访问一个变量,且这两个操作中有一个为写操作,此时这两个操作之间,存在数据依赖性,数据依赖分为以下三种:

名称代码示例说明
先写后读a=1;
b=a;
先写值,然后再读
写后写a=1;
a=2;
写一个变量,再写这个变量
读后写a=b;
b=1;
读取变量之后,重写这个变量的值

以上三种情况,只要重排序两个操作的执行顺序,程序执行的结果就会被改变。另外,编译器和处理器可能会对操作进行重排序(为了提升程序执行的效率,会按照一定的规则允许进行指令优化),但是在重排序时会遵守数据依赖性,也就是说不会改变操作执行顺序。

注意 这里所说的数据依赖性,仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑

as-if-serial

as-if-serial意思是:不管怎么重排序,程序执行结果不能被改变,编译器和处理器都必须遵循as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理不会对存在数据依赖的操作做重排序,但是操作之间不存在数据依赖的话,就有可能被编译器和处理器重排序

举个例子说明下:

int a = 3;// A
int b = 10 // B
int c = a + b; // C

上边操作的数据依赖关系是 A->C, B->C, 因此,C不能被排到A,B任意一个前边,但是A和B没有任何依赖关系,所以编译器和处理器可对A&B进行重排序,无论AB怎么重排,最终结果是不变的。

  • A -> B -> C
  • B -> A -> C

as-if-serial语义使单线程开发人员无需担心重排序会干扰他们,也无需担心内存可见性问题。

重排序对多线程的影响

现在我们来看看,重排序是否会改变多线程程序的执行结果。

class ReorderDemo {
int a = 0;

// flag 标记a是否被写入
boolean flag = false;

public void writer() {
a = 1; // 操作1
flag = true; // 操作2
}

public void reader() {
int i = 0;
if(flag) { // 操作3
i = a; // 操作4
}
System.out.printf("i: %d \n", i);
}
}

假设有两个线程A和B, A首先执行writer(),随后B接着执行reader(),B线程在执行操作4时,能否看到线程A在操作1对共享变量a的写入?
答案是: 不一定能看到

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

当操作1和2重排序,程序执行顺序如下时:
reorder01
如上所示,操作1和2做了重排序,程序执行时线程A先写变量flag,随后线程B使用了这个变量,由于条件判断为真,线程B读取a并操作,此时变量a还没有被线程A写入。这里多线程程序的语义被重排序破坏了。

当操作3和4重排序,程序执行顺序如下时:
reorder02
操作3和4存在控制依赖关系,当代码中粗在控制依赖时,会影响指令序列执行的并行度,为此,编译器和处理器会采用猜测(Speculation)执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程B的处理器可以提前读取并计算a*a,然后把计算结果临时保存到一个名为重排序缓冲(reorder buffer ROB)的硬件缓存中。当接下来操作3的条件判断为真时,就把该计算结果写入变量i中。

在单线程程序中对存在控制依赖的操作重排序,不会改变执行结果,但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

坚持原创技术分享,您的支持将鼓励我继续创作!