先行发生原则(happens-before)
我们知道,在Java内存模型中,如果要确保有序性可以靠volatile和synchronized来实现,但是如果所有的有序性都仅仅依靠这两个关键字来完成,那么有一些操作将会变得很繁琐,但是我们在编写Java代码的时候并没有感觉到这一点,这是因为Java语言中有一个“先行发生(happens-before)”的原则。那么happens-before到底是什么呢?
什么是happens-before
happens-before的概念最初由Leslie Lamport在其一篇影响深远的论文(《Time,Clocks and the Ordering of Events in a Distributed System》)中提出。Leslie Lamport使用happens-before来定义分布式系统中事件之间的偏序关系(partial ordering)。Leslie Lamport在这篇论文中给出了一个 分布式算法,该算法可以将该偏序关系扩展为某种全序关系。 JSR-133使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以是在不同线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证。
《JSR-133:Java Memory Model and Thread Specification》对happens-before关系的定义如下:
1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。注意:这一点仅仅是JMM对程序员的保证
2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。
happens-before规则示例
Java内存模型中存在一些“天然的”happens-before关系,这些先行关系无需任何同步器的协助就已经存在,可以在编码中直接使用(下面8条规则的定义参考了《深入理解Java虚拟机》一书,结合自己的理解部分规则给出了示例):
- 程序次序规则(Program Order Rule):在一个线程内,书写在前面的操作先行发生于后面的操作。准确的说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支和循环等结构。如下代码示例:1 happens-before 2,3 happens-before 4
package com.zwx.happans.before;
public class VolatileRule {
private volatile boolean flag = false;
private int a = 0;
public void writer(){
a = 1;//1
flag = true;//2
}
public void read(){
if(flag){//3
int i = a;//4
}
}
}
- volatile 变量规则(Volatile Lock Rule):对于 volatile 修饰的变量的写的操作,一定 happen-before 后续对于volatile变量的读操作。如上顺序规则示例中,因为变量flag加了volatile修饰,所以2 happens-before 3
- 传递性规则(Transitivity Rule):如上顺序规则示例中,根据顺序规则:1 happens-before 2;根据volatile变量规则:2 happens-before 3;故而根据传递性规则可以推导出:1 happens-before 3。
- 线程启动规则(Thread Start Rule):Thread对象的start()方法,先行发生行于此线程的每一个动作。如下示例中,因为1 happens-before 2,而2又happens-before线程1内的所有操作,所有x的值对线程t1是可见的
package com.zwx.happans.before;
public class StartRule {
static int x = 0;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
//主线程修改x的值对t1可见
System.out.println(x);//x=10
});
x = 10;//1
t1.start();//2
}
}
- 线程终止原则(Thread Termination Rule):线程中所有操作happens-before于对此线程的终止检测,我们可以通过Thread.join()等手段检测到线程已经终止。如下示例中,因为1 happens-before 2,而2又happens-before 3,所有t1中修改了x的值,对主线程可见
package com.zwx.happans.before;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class JoinRule {
static int x = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
x=100;//1
});
x = 10;
t1.start();
t1.join();//2
System.out.println(x);//3(x为100,join之后t1修改了x后,x对主线程可见)
}
}
- 监视器锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是必须为同一个锁,而“后面”是指的时间上的先后顺序。如下示例,假如线程A先进去执行过一次x++,然后释放锁,然后线程B再进入同步代码块,那么B获得的x为11:
package com.zwx.happans.before;
public class LockRule {
int x = 0;
public void demo(){
synchronized (this){//两个线程同时访问,相互修改的值均对对方可见
x++;
}
}
}
- 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,我们可以通过Thread.interrupted()方法检测到是否有中断发生
- 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法
如果两个操作之间的关系不在此列,并且无法从上面的关系中推导出来的话,它们就没有顺序性保障,虚拟机可以对他们随意的进行重排序,但是不管怎么随意排序,正如前面所言,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序)编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。
as-if-serial语义
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。为了具体说明,请看下面代码示例:
package com.zwx.asifserial;
public class AsIfSerialDemo {
public static void main(String[] args) {
int a = 10;//1
int b = 10;//2
int c = a+ b;//3
}
}
上面示例中:1和3之间存在数据依赖关系,同时2和3之间也存在数据依赖关系。因此在最终执行的指令序列中,3不能被重排序到1和2的前面(3排到1和2的前面,程序的结果将会被改变)。但1和2之间没有数据依赖关系,编译器和处理器可以重排序1和2之间的执行顺序。
总结
1、happens-before关系保证正确同步的多线程程序的执行结果不被改变。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
2、as-if-serial语义保证单线程内程序的执行结果不被改变。as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。
as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。