概述
本文整理自《Java并发编程的艺术》,温故而知新,加深对基础的理解程度。
顺序一致性内存模型
顺序一致性内存模型是一个理论参考模型,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参照。
顺序一致性内存模型有两大特性:
单个线程中的所有操作必须按照程序的顺序来执行。
所有线程(不管程序是否同步)都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。
顺序一致性内存模型视图如图所示:
从上面的概念图可以看到,
顺序一致性模型
有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程,同时每一个线程必须按照程序的顺序来执行内存读/写操作
,在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读/写操作串行化(即在顺序一致性模型中,所有操作之间具有全序关系)。
实例分析
假设有两个线程A和B并发执行
。其中A线程有3个操作,它们在程序中的顺序是:A1→A2→A3
。B线程也有3个操作,它们在程序中的顺序是:B1→B2→B3
。
假设这两个线程使用监视器锁
来正确同步:A线程的3个操作执行后释放监视器锁,随后B线程获取同一个监视器锁
。
那么程序在顺序一致性模型中的执行效果如图所示:
现在我们再假设这两个线程没有做同步:
那么程序在顺序一致性模型中的执行效果如图所示:
未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一个一致的整体执行顺序
。
以上图为例,线程A和B看到的执行顺序都是:B1→A1→A2→B2→A3→B3
,之所以能得到这个保证是因为顺序一致性内存模型中的每个操作必须立即对任意线程可见。
注意:
在JMM中就没有这个保证,未同步程序在JMM中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致。比如,在当前线程把写过的数据缓存在本地内存中,在没有刷新到主内存之前,这个写操作仅对当前线程可见,从其他线程的角度来观察,会认为这个写操作根本没有被当前线程执行。只有当前线程把本地内存中写过的数据刷新到主内存之后,这个写操作才能对其他线程可见。在这种情况下,当前线程和其他线程看到的操作执行顺序将不一致。
同步程序的顺序一致性
下面,看看正确同步的程序如何具有顺序一致性:
class SynchronizedExample {
int a = 0;
boolean flag = false;
public synchronized void writer() { // 获取锁
a = 1;
flag = true;
} // 释放锁
public synchronized void reader() { // 获取锁
if (flag) {
int i = a;
} // 释放锁
}
}
在上面示例代码中,假设A线程执行writer()方法后,B线程执行reader()方法,这是一个正确同步的多线程程序
。根据JMM规范,该程序的执行结果将与该程序在顺序一致性模型中的执行结果相同。
顺序一致性模型中,所有操作完全按程序的顺序串行执行。而在JMM中,临界区内的代码可以重排序。JMM会在退出临界区和进入临界区这两个关键时间点做一些特别处理,使得线程在这两个时间点具有与顺序一致性模型相同的内存视图。虽然线程A在临界区内做了重排序,但由于监视器互斥执行的特性,这里的线程B根本无法
观察
到线程A在临界区内的重排序。这种重排序既提高了执行效率,又没有改变程序的执行结果。
未同步程序的执行效果
JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。因为如果想要保证执行结果一致,JMM需要禁止大量的处理器和编译器的优化,这对程序的执行性能会产生很大的影响
。而且未同步程序在顺序一致性模型中执行时,整体是无序的
,其执行结果往往无法预知
。而且,保证未同步程序在这两个模型中的执行结果一致没什么意义。
未同步程序在JMM中的执行时,整体上是无序的,其执行结果无法预知。未同步程序在两个模型中的执行特性有如下几个差异。
- 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序)。
- 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。
JMM不保证对64位的long型和double型变量的写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性。
在一些32位的处理器上,如果要求对64位数据的写操作具有原子性,会有比较大的开销。为了照顾这种处理器,Java语言规范鼓励但不强求JVM对64位的long型变量和double型变量的写操作具有原子性。当JVM在这种处理器上运行时,可能会把一个64位long/double型变量的写操作拆分为两个32位的写操作来执行
。这两个32位的写操作可能会被分配到不同的总线事务中执行,此时对这个64位变量的写操作将不具有原子性
。
当单个内存操作不具有原子性时,可能会产生意想不到后果。请看示意图,如图所示。
如上图所示,假设处理器A写一个long型变量,同时处理器B要读这个long型变量。处理器A中64位的写操作被拆分为两个32位的写操作,且这两个32位的写操作被分配到不同的写事务中执行。同时,处理器B中64位的读操作被分配到单个的读事务中执行。当处理器A和B按上图的时序来执行时,处理器B将看到仅仅被处理器A写了一半
的无效值。
在之前的旧内存模型中,一个64位long/double型变量的读/写操作可以被拆分为两个32位的读/写操作来执行
。从即从JDK5开始,仅仅只允许把一个64位long/double型变量的写操作拆分为两个32位的写操作来执行
,任意的读操作都必须具有原子性(即任意读操作必须要在单个读事务中执行)。
参考
《Java并发编程的艺术》