一、happens-before
happens-before是JMM最核心的概念。对于Java程序员来说,理解happens-before是理解JMM的关键。
1.1 JMM的设计
从JMM设计者的角度,在设计JMM时,需要考虑两个关键因素:
1、程序员对内存模型的使用。程序员希望内存模型易于理解、易于编程。程序员希望基于一个强内存模型来编写代码。
2、编译器和处理器对内存模型的实现。编译器和处理器希望内存模型对他们的束缚越少越好,这样它们就可以尽可能多的优化来提高性能。编译器和处理器希望实现一个弱内存模型。
由于这连个因素互相矛盾,所以JSR-133专家组在设计JMM时的核心目标就是找到一个好的平衡点;一方面,要为程序员提供足够强的内存可见性保证;另一方面,对编译器和处理器的限制要尽可能地放松。下面让我们来看看JSR-133是如何实现这一目标的。
double pi=3.14; //A
double r =1.0; //B
double area=pi * r * r; //C
上面计算圆的面积的示例代码存在3个happens-before关系,如下:
A happens-before B。
B happens-before C。
A happens-before C。
在3个happens-before关系中,2和3是必需的,但1是不必要的。因此,JMM把happens-before要求禁止的重排序分为了下面两类:
1、会改变程序执行结果的重排序。
2、不会改变程序执行结果的重排序。
JMM对这两种不同性质的重排序,采取了不同的策略,如下:
1、对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
2、对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种重排序)。
JMM向程序员提供的happens-before规则能满足程序员的需求。JMM的happens-before规则不但简单易懂,而且也向程序员提供了足够强的内存可见性保证(有些内存可见性保证其实并不一定真实存在,比如上面的A happens-before B)。
JMM对编译器和处理器的束缚已经尽可能少。JMM其实在遵守一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都可以。
1.2 happens-before的定义
1、如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2、两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法。
上面的1是JMM对程序员的承诺。从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证-A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证!
上面的2是JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM其实是在遵守一个基本原则:只要不改变程序的执行结果,编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的是被重排序并不关心,程序员关心的是程序执行时的语义不能被改变,即执行结果不能被改变。因此,happens-before关系本质上和as-if-serial语义是一回事。
as-if-serial语义保证单线程内程序的执行结果不被改变,happens-bofore关系保证正确同步的多线程程序的执行结果不被改变。
as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
1.3 happens-before规则
1、程序员规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
2、监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
3、volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
4、传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
5、start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
6、join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
二、双重检查锁定与延迟初始化
略
三、Java内存模型综述
3.1 各种内存模型之间的关系
JMM是一个语言级的内存模型,处理器内存模型是硬件级的内存模型,顺序一致性内存模型是一个理论参考模型。常见的处理器内存模型要比常见的语言级内存模型弱,处理器内存模型和语言级内存模型都要比顺序一致性内存模型弱。同处理器内存模型一样,越是追求执行性能的语言,内存模型设计的会越弱。
3.2 JMM的内存可见性保证
按程序类型,Java程序的内存可见性保证可以分为下列3类。
1、单线程程序。单线程程序不会出现内存可见性问题。编译器、runtime和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。
2、正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。这是JMM关注的重点,JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。
3、未同步/未正确同步的多线程程序。JMM为他们提供了最小安全性保障;线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false)。
3.3 JSR-133对旧内存模型的修补
JSR-133对JDK 5之前的旧内存模型的修补主要有两个:
1、增强volatile的内存语义。旧内存模型允许volatile变量与普通变量重排序。JSR-133严格限制volatile变量与普通变量的重排序,使volatile的写-读和锁的释放-获取具有相同的内存语义。
2、增强final的内存语义。在旧内存模型中,多次读取同一个final变量的值可能会不相同。为此,JSR-133为final增加了两个重排序规则。在保证final引用不会从构造函数内逸出的情况下,final具有了初始化安全性。
总结
本章对Java内存模型做了比较全面的解读,有助于解决在Java并发编程中经常遇到的各种内存可见性问题。