阅读更多
1 前言
happens-before是JMM最核心的概念
- 对应Java程序员来说,理解happens-before是理解JMM的关键
- JSR-133使用happens-before的概念来阐述操作之间的内存可见性
- 在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系(这里提到的两个操作既可以在一个线程之内,也可以是在不同线程之间)
2 JMM的设计
在设计JMM时,需要考虑两个关键因素
- 程序员对内存模型的使用。程序员希望内存模型更易于理解、易于编程。程序员希望基于一个强内存模型来编写代码
- 编译器和处理器对内存模型的实现。编译器和处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。编译器和处理器希望实现一个弱内存模型
上述两个因素相互矛盾,所以JSR-133专家组在设计JMM内存模型时的核心目标就是找到一个好的平衡点:
- 一方面,要为程序员提供足够强的内存可见性保证
- 另一方面,对编译器和处理器的限制要尽可能放松
JMM把happens-before要求禁止的重排序分为了下面两类
- 会改变程序结果的重排序
- 不会改变程序结果的重排序
JMM对这两种不同性质的重排序,采取了不同的策略
- 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序
- 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种重排序)
JMM其实遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行
- 例如,如果编译器经过细致分析后,认定一个锁只会被单个线程访问,那么这个锁可以被消除
- 再如,如果编译器经过细致分析后,认定一个volatile变量只会被单个线程访问,那么编译器可以把这个volatile变量当做一个普通变量来对待
- 这些优化既不会改变程序的执行结果,又能提高程序的执行效率
什么叫做正确同步?我的理解是:对于一个或者几个共享变量,同步会使得这些共享变量的访问操作串行化,于是在同一时刻只有一个线程能够访问这些变量。如果我们不关心具体的线程,那么正确同步的多线程程序也可以"理解为或等效于"一个单线程程序
3 happens-before 定义
JSR-133使用happens-before的概念来指定两个操作之间的执行顺序,由于这两个操作可以在一个线程之内,也可以在不同线程之间。因此JMM,通过happens-before关系向程序员提供跨线程的内存可见性保证
《JSR-133:Java Memory Model and Thread Specification》对happens-before关系的定义如下
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
- 第一个关键点:可见性
- 第二个关键点:执行顺序(但是第二条规则可能会破坏这种执行顺序)
- 这条规则是JMM对程序员的承诺,从程序员的角度可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型向程序员保证–A操作的结果将对B可见,且A的执行顺序(可以理解为,但真实是否如此,要看编译器和处理器是否进行优化)排在B之前。注意,这只是JMM向程序员做出的保证
- 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,域按happens-before关系来执行的结果一致,那么这种重排序并不非法(JMM允许这种重排)
- 这条规则是JMM对编译器和处理器重排序的约束原则。前面提到过,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这样做的原因是:程序员对于这两个操作是否真的被重排并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)
本质上,happens-before关系和as-if-serial语义是一回事
- as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的结果不被改变
- as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按照程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的
4 happens-before规则
程序次序规则(Program Order Rule)
:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作,准确的说,应该是控制流顺序而不是程序代码顺序管程锁定规则(Monitor Lock Rule)
:一个unlock操作线性发生于后面对同一个锁的lock操作volatile变量规则(Volatile Variable Rule)
:对一个volatile变量的写操作先行发生于后面这个变量的读操作,后面指时间上的先后顺序线程启动规则(Thread Start Rule)
:Thread对象的start()方法先行发生于此线程的每一个动作线程终止规则(Thread Termination Rule)
:线程中所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等手段检测到线程已经终止执行线程中断规则(Thread Interruption Rule)
:对线程interrupt()方法调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生对象终结规则(Finalizer Rule)
:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始传递性(Transitivity)
:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论
5 参考
- 《Java并发编程的艺术》