Java并发编程的艺术学习笔记(三) Java内存模型(三)

3.3 顺序一致性

顺序一致性内存模型是一个理论参考模型,在设计的时候,处理器的内存模型和编程语
言的内存模型都会以顺序一致性内存模型作为参照。

3.3.1 数据竞争与顺序一致性

当程序未正确同步时,就可能会存在数据竞争。Java内存模型规范对数据竞争的定义如
下。
在一个线程中写一个变量,
在另一个线程读同一个变量,
而且写和读没有通过同步来排序。
当代码中包含数据竞争时,程序的执行往往产生违反直觉的结果(前一章的示例正是如此)。如果一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序。
JMM对正确同步的多线程程序的内存一致性做了如下保证。
如果程序是正确同步的,程序的执行将具有顺序一致性(Sequentially Consistent)——即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。马上我们就会看到,这对于程序员来说是一个极强的保证。这里的同步是指广义上的同步,包括对常用同步原语 (synchronized、volatile和final)的正确使用。

3.3.2 顺序一致性内存模型

顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性。
1)一个线程中的所有操作必须按照程序的顺序来执行。
2)(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内
存模型中,每个操作都必须原子执行且立刻对所有线程可见。

Java并发编程的艺术学习笔记(三) Java内存模型(三)_第1张图片

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

Java并发编程的艺术学习笔记(三) Java内存模型(三)_第2张图片

Java并发编程的艺术学习笔记(三) Java内存模型(三)_第3张图片

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

但是,在JMM中就没有这个保证。未同步程序在JMM中不但整体的执行顺序是无序的,而
且所有线程看到的操作执行顺序也可能不一致。

3.3.3 同步程序的顺序一致性效果

Java并发编程的艺术学习笔记(三) Java内存模型(三)_第4张图片

在上面示例代码中,假设A线程执行writer()方法后,B线程执行reader()方法。这是一个正 确同步的多线程程序。

Java并发编程的艺术学习笔记(三) Java内存模型(三)_第5张图片

从这里我们可以看到,JMM在具体实现上的基本方针为:在不改变(正确同步的)程序执行结果的前提下,尽可能地为编译器和处理器的优化打开方便之门。

3.3.4 未同步程序的执行特性

对于未同步或未正确同步的多线程程序,JMM只提供最小安全性:线程执行时读取到的 值,要么是之前某个线程写入的值,要么是默认值(0,Null,False),JMM保证线程读操作读取 到的值不会无中生有(Out Of Thin Air)的冒出来。

JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。因为 如果想要保证执行结果一致,JMM需要禁止大量的处理器和编译器的优化,这对程序的执行性能会产生很大的影响。而且未同步程序在顺序一致性模型中执行时,整体是无序的,其执行结果往往无法预知。而且,保证未同步程序在这两个模型中的执行结果一致没什么意义。
未同步程序在JMM中的执行时,整体上是无序的,其执行结果无法预知。未同步程序在两
个模型中的执行特性有如下几个差异。
1)顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的
操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序)。这一点前
面已经讲过了,这里就不再赘述。
2)顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程
能看到一致的操作执行顺序。这一点前面也已经讲过,这里就不再赘述。
3)JMM不保证对64位的long型和double型变量的写操作具有原子性,而顺序一致性模型保 证对所有的内存读/写操作都具有原子性。
第3个差异与处理器总线的工作机制密切相关。

处理器与内存之间的数据传递的一系列步骤称为总线事务,包括读事务和写事务。当多个处理器同时发起总线事务时,总线仲裁会确保所有处理器都能公平的访问内存。总线的这些工作会把所有处理器对内存的访问以串行化的方式来执行。在任意时间点,最多只能有一个处理器可以访问内存。这个特性确保了单个总线事务之中的内存读写操作具有原子性。

在32位处理器上,可能会把一个64位的long/double型变量的写操作分为两个32位的写操作来执行。这两个写操作可能会被分配到不同的总线事务中执行,此时对这个64位变量的写操作将不具有原子性。                                                                                                                                            

你可能感兴趣的:(并发编程,Java)