11.1.2 Java内存模型

Java内存模型(Java Memory Model)描述了程序中共享变量的关系以及在主存中写入和读取这些变量值的底层细节。Java内存模型作为一个抽象模型,只关注主存中的共享变量。只关注主存可以对开发人员屏蔽CPU缓存等细节,简化内存模型自身的定义。对象的实例域、静态域和数组元素存储在堆内存中,可以被多个线程共享。这些变量是内存模型需要关注的内容。局部变量、方法的形式参数和异常处理时的异常参数都是不被共享的,不会受到内存模型的影响。当对共享变量的两个访问中至少有一个是写入操作时,这两个访问存在冲突。两个互相冲突的访问的执行结果由内存模型来确定。

在一个单线程程序的运行过程中,指令的执行结果是可预期的。编译器有可能会对某些指令进行重排,但这些操作不会影响程序的行为。在一个多线程程序中,线程所执行的动作可以分成两类:一类是线程内部的动作,比如操作方法中的局部变量等;另外一类是线程之间的动作。线程之间的动作是由一个线程产生的动作,同时可以被另外一个线程检测到或受到另外一个线程的直接影响。线程之间的动作包括读取和写入共享变量以及加锁和解锁等同步操作。线程内部的动作不会产生可见性相关的问题,因此内存模型只考虑线程之间的动作。

在一个线程的运行过程中,如果假设该线程处于隔离状态,即其他线程都不存在时,那么该线程所产生的所有线程之间的动作会按照某个顺序来执行。这个执行顺序由程序内部的代码逻辑来确定,称为程序顺序(program order)。在多个线程同时运行时,所产生的线程间的动作会交织在一起,形成实际的执行顺序。如果这个实际的执行顺序与每个线程隔离运行时的程序顺序保持一致,同时每个读取操作所看到的都是最近一次写入操作的执行结果,则称这个执行顺序是具备顺序一致性的。顺序一致性是一个非常严格的约束。如果Java内存模型要求动作的执行顺序满足顺序一致性的要求,则很多CPU和编译器进行的优化措施都是不允许的。

线程间的动作中的很大一部分是同步动作。如果只考虑同步动作,则将这些动作的执行顺序称为同步顺序(synchronization order)。同步动作的出现在动作之间形成了同步关系。同步关系定义了同步动作之间的先后顺序。这种顺序是强制的。以加锁动作为例,在成功加锁之前,对应的锁要被释放,否则加锁动作无法成功。所以成功的加锁动作必然在某个解锁动作之后。常见的同步关系如下所示,其中“A与B保持同步”的含义是A必然发生在B之前。

1)在一个监视器对象上的解锁动作与相同对象上后续成功的加锁动作保持同步。

2)对一个声明为volatile的变量的写入动作与同一变量的后续读取动作保持同步。

3)启动一个线程的动作与该线程执行的第一个动作保持同步。

4)向线程中共享变量写入默认值的动作与该线程执行的第一个动作保持同步。这种同步关系的含义是在线程运行之前,该线程所使用的全部对象从概念上说已经被创建出来,并且对象中的变量被赋值为默认值。这种同步关系的含义是保证线程所看到的变量的值是确定的。变量的值可能是根据变量类型确定的默认值,也可能是其他线程所设置的值,但不可能是其他值。

5)线程A运行时的最后一个动作与另外一个线程中任何可以检测到线程A终止的动作保持同步。

6)如果线程A中断线程B,那么线程A的中断动作与任何其他线程检测到线程B处于被中断的状态的动作保持同步。

除了程序顺序和同步顺序之外,还有一种更加实用的顺序,即“在之前发生(happens-before)”顺序。如果一个动作按照“在之前发生”的顺序发生在另外一个动作之前,那么前一个动作的执行结果在多线程程序中对后一个动作是肯定可见的。“在之前发生”顺序的情况包括:

1)如果两个动作A和B在一个线程中执行,同时在程序顺序中A出现在B之前,则A在B之前发生。

2)一个对象的构造方法的结束在该对象的finalize方法运行之前发生。

3)如果动作A和动作B保持同步,则A在B之前发生。

4)如果动作A在动作B之前发生,同时动作B在动作C之前发生,则A在C之前发生。也就是说,“在之前发生”顺序是传递性的。

如果程序中存在对共享变量的互相冲突的访问,且这些访问没有通过“在之前发生”顺序来正确排列时,那么程序中存在数据竞争(data race)。数据竞争的存在是多线程程序运行时产生错误的根源。编写多线程程序时的首要任务是找出并解决程序中存在的数据竞争。代码清单11-1中的getNext方法在调用时存在数据竞争。不同线程都需要读写共享变量value的值,但是这些访问动作没有定义“在之前发生”顺序,因此程序运行可能出现错误。如果程序中不存在数据竞争,则程序的任何执行方式都具备顺序一致性。开发人员需要做的是利用Java平台提供的支持来消除程序中的数据竞争,这样就可以保证程序的正确性。不需要考虑CPU和编译器可能进行的指令重排,因为Java虚拟机和编译器会确保这些指令重排不会影响程序的正确性。