happens-before 原则 (先行发生原则)是JMM中最核心的概念,该原则阐述了操作之间的内存可见性。
happens-before的诞生——完善的JMM
Java语言是最早尝试提供内存模型的语言,这是简化多线程编程、保证程序可移植性的一个飞跃。早起类似C、C++等语言,并不存在内存模型的概念,其行为依赖于处理器本身的内存一致性模型,但不同的处理器差异很大,所以一段C++程序在处理器A上正常运行,并不能保证在B上也能得到同样的结果。
Java对于内存模型的提出无疑具有划时代的意义,但是问题的复杂度远远被低估了,随着Java程序运行在越来越多的平台之上,过于泛泛的内存模型定义会出现很多模棱两可的地方,对于synchronized和volatile的指令重排序问题等也没有做出明确规范。总结来说就是:
-
不能保证一些多线程程序的正确性(双检锁失效问题)
-
不能保证同一段程序在不同处理器架构上表现一致,例如有的处理器支持缓存一致性,有的不支持,各自都有自己的内存模型
基于以上的问题,Java迫切需要一个完善的JMM,能够让普通Java开发者和编译器、JVM工程师达成清晰的共识,可以相对简单并准确的判断多线程程序什么样的执行序列是符合规范的。
-
对于编译器、JVM工程师,关注点是如何使用类似内存屏障之类的技术,保证执行结果符合JMM的推断
-
对于Java应用开发者,可能更加关注于volatile、synchronized等语义,如何利用类似 happens-before 规则,写出可靠的多线程应用
下面是角色分工图:
happens-before 是什么?
happens-before(先行发生原则) 是Java内存模型用来保证多线程操作可见性的机制。先行发生是Java内存模型中定义的两项操作之间的偏序关系:如果A操作先行发生于B操作,则操作A产生的影响将被B观察到,其中影响包括:修改了内存中共享变量的值、发送了消息、调用了方法等。
我们知道,虚拟机为了提高程序执行的效率,可能会对程序进行重排序,例如下面的示例:
//计算一个矩形的面积。公式:S = a * b
double a = 3; // A
double b = 5; // B
double S = a * b; // C
由于A和B操作之间没有任何依赖关系,那么A和B操作之间的顺序是可以由虚拟机进行重排序的,但是B和C之间却禁止重排序,因为B和C之间存在数据依赖,如果进行重排序将会对结果产生影响。在单线程程序中,这种语义保证被称为 as-if-serial语义,而在多线程执行程序中,就是 happens-before关系。
其实happens-before关系本质上就是as-if-serial语义:
-
as-if-serial语义保证单线程内程序的执行结果不会改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变
-
as-if-serial语义让编写单线程程序的程序员有一种错觉:单线程程序是按顺序执行的。happens-before关系让编写多线程程序的程序员有一种错觉:正确同步的多线程程序是按happens-before指定的顺序来执行的
在上面示例代码中存在着3个happens-before关系:
-
A happens-before B (程序顺序)
-
B happens-before C (程序顺序)
-
A happens-before C (传递性)
JMM 把 happens-before 要求禁止的重排序分为如下两类:
-
会改变程序执行结果的重排序(JMM要求编译器和处理器必须禁止这种重排序)
-
不会改变程序执行结果的重排序(JMM对编译器和处理器不做要求——JMM允许这种重排序)
happens-before 规则
下面介绍了JMM中一些天然存在的happens-before关系,这些happens-before关系无需任何同步协助器就已经存在,可以直接使用。
-
程序次序规则: 一个线程中的每个操作,先行发生于该线程的任意后续操作(从程序控制流顺序考虑)
-
监视器锁规则: 一个unlock操作先行发生于后面对同一个锁的lock操作
-
volatile变量规则: 对于一个volatile变量的写操作先行发生于后面对这个变量的读操作
-
线程启动规则: Thread对象的start()方法先行发生于此线程的每一个动作(例如:如果线程A执行了ThreadB.start(),那么线程A的ThreadB.start()操作先行发生于B中的任意操作)
-
线程终止规则: 线程的所有操作都先行发生于对此线程的终止检测(例如:如果线程A执行了ThreadB.join()并成功返回,那么B中的所有操作都先行发生于A从ThreadB.join()的成功返回)
-
线程中断规则: 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
-
对象终结规则: 一个对象的初始化完成(执行构造函数结束)先行发生于它的finalize()方法的开始
-
传递性: 如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C
参考
《并发编程的艺术》
《深入理解Java虚拟机》