JMM的顺序一致性

为了解决并发三要素的问题,JAVA尝试使用一种“JAVA内存模型”来屏蔽各种硬件和操作系统的内存访问差异,以实现让JAVA程序在各平台下都能达到一致的内存访问效果。

JMM的定义

给定一个程序和该程序的一串执行轨迹,内存模型描述了该执行轨迹是否是该程序的一次合法执行。对于Java,内存模型检查执行轨迹中的每次读操作,然后根据特定规则,检验该读操作观察到的写是否合法。

内存模型描述了某个程序的可能行为。JVM实现可以自由地生成想要的代码,只要该程序所有最终执行产生的结果能通过内存模型进行预测。这为大量的代码转换提供了充分的自由,包括动作(action)的重排序以及非必要的同步移除。

内存模型的一个高级、非正式的概述显示其是一组规则,规定了一个线程的写操作何时会对另一个线程可见。通俗地说,读操作r通常能看到任何写操作w写入的值,意味着w不是在r之后发生,且w看起来没有被另一个写操作w’覆盖掉(从r的角度看)。

在本内存模型规范中使用“读取(read)”这个词时,仅是指读取字段或数组元素的动作(action)。其它操作的语义,如读取数组的长度,执行受检转换以及虚方法调用,都不会被数据争用直接影响到。JVM实现有责任确保数据争用不会导致诸如返回错误的数组长度或调用虚方法导致段错误这类不正确的行为。

内存语义决定着程序中每个时刻能读到的值。每个单个线程中的动作(action)必须表现为被该线程的语义所控制,不包括读操作看到的值由内存模型决定的情况。当指的是这种情景时,我们说该程序遵守线程内(intra-thread)语义。

官网提出的定义还是很晦涩难懂的,也能这个模型的不好定义。在经历了反复修正在之后,直至JDK5实现了JSR-133后,才逐步的成熟起来,但是依旧不好理解。

数据竞争与顺序一致性

当程序未正确同步时,就会存在数据竞争(data race)。java 内存模型规范对数据竞争的定义如下:

  • 在一个线程中写一个变量,
  • 在另一个线程读同一个变量,
  • 而且写和读没有通过同步来排序。

当代码中包含数据竞争时,程序的执行往往产生违反直觉的结果(效果参考重排序章节)。如果一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序。

顺序一致性是程序执行过程中可见性和顺序的强有力保证。在顺序一致的执行过程中,所有操作(如读和写)间存在一个全序关系,与程序的顺序一致。每个操作都是原子的且立即对所有线程可见。如果一个程序没有数据竞争,那么该程序的执行看起来将是顺序一致的。

如果一组操作要保持原子性而未得到保证时,无论它的顺序一致性和数据竞争是怎样的状态,仍然可能会出现错误。所以内存模型中操作不仅仅需要有顺序一致性,还需要有原子性。

JMM的参考模型

顺序一致性内存模型

在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程。同时,每一个线程必须按程序的顺序来执行内存读 / 写操作。从上图我们可以看出,在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读 / 写操作串行化。

顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性:

  • 在顺序一致的执行过程中,所操作(如读和写)间存在一个全序关系,与程序的顺序一致。
  • (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

通过示意图来理解这两个特性

假设有两个线程 A 和 B 并发执行。其中 A 线程有三个操作,它们在程序中的顺序是:A1->A2->A3。B 线程也有三个操作,它们在程序中的顺序是:B1->B2->B3。

假设这两个线程使用管程来正确同步:A 线程的三个操作执行后释放监视器,随后 B 线程获取同一个监视器。那么程序在顺序一致性模型中的执行效果将如下图所示:

在这里插入图片描述

再假设这两个线程没有做同步:

在这里插入图片描述

未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一个一致的整体执行顺序。以上图为例,线程 A 和 B 看到的执行顺序都是:B1->A1->A2->B2->A3->B3。之所以能得到这个保证是因为顺序一致性内存模型中的每个操作必须立即对任意线程可见。

在 JMM 中就没有内存可见性这个保证。未同步程序在 JMM 中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致。比如,在当前线程把写过的数据缓存在本地内存中,且还没有刷新到主内存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,会认为这个写操作根本还没有被当前线程执行。只有当前线程把本地内存中写过的数据刷新到主内存之后,这个写操作才能对其他线程可见。在这种情况下,当前线程和其它线程看到的操作执行顺序将不一致。

happens-before

happens-before在JMM中定义了两个操作之间的偏序关系。

在JSR-133中正式介绍JMM之前 ,展示了一个比较简单的内存模型,就是happens-before内存模型。在JSR-133对happens-before关系有以下定义:

  1. 如果操作Ahappens-before操作B,那么操作A的执行结果对操作B可见,且操作A的执行顺序在操作B之前。
  2. 操作A、B间存在happens-before关系,并不代表Java平台的具体实现按happens-before关系指定的顺序执行,如果重排序之后的执行结果,与按happens-before来执行结果一直,那么重排序就不非法。

happens-before的8个规则

  • 程序次序规则(Program Order Rule):在一个线程中,按照控制流顺序,书写在前面的操作优先发生于书写在后面的操作。
  • 管程锁定规则(Monitor Rule):一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
  • volatile变量规则(Volatile Variable Rule):对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
  • 线程启动规则(Thread strat Rule):Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
  • 线程终止规则(Thread Termination Rule):线程中所有的操作都先行发生于对此线程的终止检测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是都已经终止,也就是说线程的所有操作,都优先于Thread::join()或Thread::isAlive()。这个规则也叫Thread Join Rule,Thread 对象的结束先行发生于 join() 方法返回。
  • 线程中断规则(Thread Interruption Rule):对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。
  • 对象终结规则(Finalizer rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
  • 传递性(Transitivity):如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。

主要来自《Java并发编程的艺术》 方腾飞 魏鹏 程晓明版

JMM 对正确同步的多线程程序的内存一致性做了如下保证:

  • 如果程序是正确同步的,程序的执行将具有顺序一致性(sequentially consistent)-- 即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。这里的同步是指广义上的同步,包括对常用同步原语(lock,volatile 和 final)的正确使用。