Java语言规范(JLS)在第17.4节中介绍了JMM。其中的描述非常正式,用同步动作和被称为偏序1的数学结构描述JMM。这对于语言理论学家和Java规范的实现者(编译器和虚拟机的制造者)来说非常棒,但对于需要理解多线程代码如何执行的应用开发人员来说,这种描述会让他们头昏脑胀。
1 设A是一个非空集,P是A上的一个关系,若关系P是自反的(对任意的a∈A,(a,a)∈P)、反对称的(若(a,b)∈P且(b,a)∈P,则a=b)和传递的(若(a,b)∈P,(b,c)∈P,则(a,c)∈P),则称P是集合A上的偏序关系。比如实数集上的小于等于关系(a<=a;a<=b,b<=a,则a=b;a<=b,b<=c,则a<=c)。——译者注
我们在这里不重复规范里的正式描述,而是用两个基本概念列出最重要的规则,这两个概念是代码块之间的之前发生(Happens-Before)和同步约束(Synchronizes-With)关系。
- 之前发生——这种关系表明一段代码块在其他代码开始之前就已经全部完成了。
- 同步约束——这意味着动作继续执行之前必须把它的对象视图与主内存进行同步。
如果你认真研究过OO编程,应该听到过面向对象构件的Has-A和Is-A这两种表述方式。一些开发人员发现,用之前发生和同步约束来描述基本的概念构件对理解Java并发很有帮助。这和Has-A与Is-A的道理一样,但这两组概念在技术上没有直接关联。
图4-11中是一个易失性写入与后续的读取访问(用于println
)之间同步约束的例子。
图4-11 同步约束的例子
JMM的主要规则如下。
- 在监测对象上的解锁操作与后续的锁操作之间存在同步约束关系。
- 对易失性(volatile)变量的写入与后续对该变量的读取之间存在同步约束关系。
- 如果动作A受到动作B的同步约束,则A在B 之前发生。
- 如果在程序中的线程内A出现在B之前,则A在B 之前发生。
前两个规则通俗来说就是“先放后取”。换句话说,一个线程在写入时持有的锁要在其他操作(包括读取)能够获取锁之前被释放掉。
这里还有些规则,实际上是关于敏感行为的。
- 构造方法要在那个对象的终结器开始运行之前完成(一个对象被终结之前必须已经构造完整)。
- 开始一个线程的动作受到这个新线程的第一个动作的同步制约。
Thread.join
受到被合并的线程的最后一个(和其他全部)动作的同步制约。- 如果X在Y 之前发生,并且Y在Z 之前发生,则X在Z 之前发生(传递性)。
这些简单的规则定义了内存和同步如何工作的全平台视图。图4-12展示了传递性规则。
图4-12 之前发生的传递性
注意 实际上,这些规则是JMM做出的最低保证。真正的JVM实际上可能表现得更好。对于开发人员来说,这可能是个陷阱,因为某个特定JVM中的行为实际上是个隐藏在底层并发中的诡异bug,却很容易给人造成错觉,以为是它提供的安全特性。
从这些最低保证中,很容易可以看出不可变性成为Java并发编程中的一个重要概念的原因。如果对象不可改变,确保改变对所有线程可见的相关问题就不会出现。