基础铺垫
为了文章的顺利进行,必须得进行一个基础知识的铺垫,那就是 Happens-Before 关系。
而 Happens-Before 关系的正式提出,就是 jsr 133 规范:
如果你不知道 jsr133 是啥,那么可以去这个链接里面看看。
在这里面就有大家耳熟能详的 Happens-Before 关系的正式描述,大家看到的所有的中文版翻译的原文,就是这里:
由于这段话,特别是那六个小黑点后面的话太重要了,失之毫厘谬以千里,所以我不敢轻易按照之前的轻松风格大致翻译。
于是我决定站在大佬的肩膀上,分别把《深入理解Java虚拟机(第三版)》、《Java并发编程实战》、《Java并发编程的艺术》这三本书中关于这部分的定义和描述搬运一下,大家对比着看。
如果对于该规则了然于心,可以跳过本小节。
走起。
首先是《深入理解Java虚拟机(第三版)》:
- 程序次序规则(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的结论。
接着是《Java并发编程实战》:
- 程序顺序规则:如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前执行。
- 监视器锁规则:在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行。
- volatile 变量规则:对volatile 变量的写入操作必须在对该变量的读操作之前执行。
- 线程启动规则:在线程上对Thread.Start的调用必须在该线程中执行任何操作之前执行。
- 线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从Thread.join中成功返回,或者在调用Thread.isAlive时返回 false.
- 中断规则:当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行(通过抛出InterruptedException,或者调用isInterrupted和interrupted).
- 终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成。
- 传递性:如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A必须在操作C之前执行。
《Java并发编程的艺术》,在这本书里面作者加了一个限定词“与程序员密切相关的 happens-before规则如下”:
- 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
- volatile 变量规则:对一个volatile域的写,happens-before 于任意后续对这个 volatile 域的读。
- 传递性:如果 A happens-before B,且B happens-before C,那么 A happens-before C。
也就是说:线程启动规则、线程结束规则、中断规则、对象终结规则其实对于开发来说是无感的,在这几个规则里面,我们没有什么可以搞事的空间。
当你把这三本书中对于同一件事情的描述对比着来看的时候,也许会稍微的印象深刻一点吧。
本质上说的是一回事,只是描述略有不同而已。
另外,我觉得我需要补充一个我觉得非常重要的点,那就是在原文论文中多处出现的一个非常重要的单词 action:
那么啥是 action?
对于这个略显模糊的定义,论文开篇的第五点提到了具体含义:
In this section we define in more detail some of the informal concepts we have presented.
在本节中,我们将更详细地定义一些我们提出的非正式概念。
其中对论文中的七个概念进行了详细描述,分别是:
- Shared variables/Heap memory
- Inter-thread Actions
- Program Order
- Intra-thread semantics
- Synchronization Actions
- Synchronization Order
- Happens-Before and Synchronizes-With Edges
其中,我个人理解,happens-before 中的 action 主要是说下面这个三个概念:
线程间(inter-thread)动作、线程内(intra-thread)动作、同步动作(Synchronization Actions)。
加锁、解锁、对 volatile 变量的读写、启动一个线程以及检测线程是否结束这样的操作,均为同步动作。
而线程间(inter-thread)动作与线程内(intra-thread)动作是相对而言的。比如一个线程对于本地变量的读写,也就是栈上分配的变量的读写,是其他线程无法感知的,这是线程内动作。而线程间动作比如对于全局变量的读写,也就是堆里面分配的变量,其他线程是可以感知的。
另外,你看 Inter-thread Actions 里面我画下划线的地方,描述其实和同步动作相差无几。我理解,其实线程间动作大多也就是同步动作。
所以你去看一本书,叫做《深入理解Java虚拟机HotSpot》,这本书里面对于happens-before 的描述就稍微有点不一样了,开篇加的限定条件就是“所有同步动作...”:
1)所有同步动作(加锁、解锁、读写volatile变量、线程启动、线程完成)的代码顺序与执行顺序一致,同步动作的代码顺序也叫作同步顺序。
1.1)同步动作中对于同一个monitor,解锁发生在加锁前面。
1.2)同一个volatile变量写操作发生在读操作前面。
1.3)线程启动操作是该线程的第一个操作,不能有先于它的操作发生。
1.4)当T2线程发现T1线程已经完成或者连接到T1,T1的最后一个操作要先于T2 所有操作。
1.5)如果线程T1中断线程T2,那么T1中断点要先于任何确定T2被中断的线程的 操作。
对变量写入默认值的操作要先于线程的第一个操作;对象初始化完成操作要先 于finalize()方法的第一个操作。
2)如果a先于b发生,b先于c发生,那么可以确定a先于c发生。
3)volatile的写操作先于volatile的读操作。
本来,我还想举出《Java编程思想》里面关于 happens-before 的描述的。
结果,我翻完了书中关于并发的部分,结果它:
没,有,写!
好吧,我想有可能这本神书写于 2004 年 jsr133 发布之前?
结果,它的英文版发布时间是在 2006 年,也就是作者故意没写的,他只是在 21.11.1 章节里面提到了《Java Concurreny in Practice》: