java内存模型-顺序一致性
数据竞争与顺序一致性保证
当程序未正确同步时,就会存在数据竞争。java 内存模型规范对数据竞争的定义如下:
- 在一个线程中写一个变量,
- 在另一个线程读同一个变量,
- 而且写和读没有通过同步来排序。
当代码中包含数据竞争时,程序的执行往往产生违反直觉的结果(前一章的示例正是如此)。如果一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序。
JMM 对正确同步的多线程程序的内存一致性做了如下保证:
- 如果程序是正确同步的,程序的执行将具有顺序一致性(sequentially consistent)–即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同(马上我们将会看到,这对于程序员来说是一个极强的保证)。这里的同步是指广义上的同步,包括对常用同步原语(lock,volatile 和 final)的正确使用。
顺序一致性内存模型
顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性:
- 一个线程中的所有操作必须按照程序的顺序来执行。
- (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。
顺序一致性内存模型为程序员提供的视图如下:

在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程。同时,每一个线程必须按程序的顺序来执行内存读/写操作。从上图我们可以看出,在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读/写操作串行化。
为了更好的理解,下面我们通过两个示意图来对顺序一致性模型的特性做进一步的说明。
假设有两个线程A和B并发执行。其中 A 线程有三个操作,它们在程序中的顺序是:A1->A2->A3。B线程也有三个操作,它们在程序中的顺序是:B1->B2->B3。
假设这两个线程使用监视器来正确同步:A 线程的三个操作执行后释放监视器,随后 B 线程获取同一个监视器。那么程序在顺序一致性模型中的执行效果将如下图所示:

现在我们再假设这两个线程没有做同步,下面是这个未同步程序在顺序一致性模型中的执行示意图:

未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一个一致的整体执行顺序。以上图为例,线程 A 和 B 看到的执行顺序都是:B1->A1->A2->B2->A3->B3。之所以能得到这个保证是因为顺序一致性内存模型中的每个操作必须立即对任意线程可见。
但是,在 JMM 中就没有这个保证。未同步程序在 JMM 中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致。比如,在当前线程把写过的数据缓存在本地内存中,且还没有刷新到主内存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,会认为这个写操作根本还没有被当前线程执行。只有当前线程把本地内存中写过的数据刷新到主内存之后,这个写操作才能对其他线程可见。在这种情况下,当前线程和其它线程看到的操作执行顺序将不一致。
同步程序的顺序一致性效果
下面我们对前面的示例程序 ReorderExample 用监视器来同步,看看正确同步的程序如何具有顺序一致性。
请看下面的示例代码:
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 不允许临界区内的代码“逸出”到临界区之外,那样会破坏监视器的语义)。JMM会在退出监视器和进入监视器这两个关键时间点做一些特别处理,使得线程在这两个时间点具有与顺序一致性模型相同的内存视图(具体细节后文会说明)。虽然线程 A 在临界区内做了重排序,但由于监视器的互斥执行的特性,这里的线程 B 根本无法“观察”到线程 A 在临界区内的重排序。这种重排序既提高了执行效率,又没有改变程序的执行结果。
从这里我们可以看到 JMM 在具体实现上的基本方针:在不改变(正确同步的)程序执行结果的前提下,尽可能的为编译器和处理器的优化打开方便之门。
未同步程序的执行特性
对于未同步或未正确同步的多线程程序,JMM 只提供最小安全性:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false),JMM 保证线程读操作读取到的值不会无中生有(out of thin air)的冒出来。为了实现最小安全性,JVM 在堆上分配对象时,首先会清零内存空间,然后才会在上面分配对象(JVM内部会同步这两个操作)。因此,在以清零的内存空间(pre-zeroed memory)分配对象时,域的默认初始化已经完成了。
JMM 不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。因为未同步程序在顺序一致性模型中执行时,整体上是无序的,其执行结果无法预知。保证未同步程序在两个模型中的执行结果一致毫无意义。
和顺序一致性模型一样,未同步程序在 JMM 中的执行时,整体上也是无序的,其执行结果也无法预知。同时,未同步程序在这两个模型中的执行特性有下面几个差异:
- 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而 JMM 不保证单线程内的操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序)。这一点前面已经讲过了,这里就不再赘述。
- 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。这一点前面也已经讲过,这里就不再赘述。
- JMM不保证对64位的 long 型和 double 型变量的读/写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性。
第3个差异与处理器总线的工作机制密切相关。在计算机中,数据通过总线在处理器和内存之间传递。每次处理器和内存之间的数据传递都是通过一系列步骤来完成的,这一系列步骤称之为总线事务(bus transaction)。总线事务包括读事务(read transaction)和写事务(write transaction)。读事务从内存传送数据到处理器,写事务从处理器传送数据到内存,每个事务会读/写内存中一个或多个物理上连续的字。这里的关键是,总线会同步试图并发使用总线的事务。在一个处理器执行总线事务期间,总线会禁止其它所有的处理器和 I/O 设备执行内存的读/写。下面让我们通过一个示意图来说明总线的工作机制:

如上图所示,假设处理器 A,B 和 C 同时向总线发起总线事务,这时总线仲裁(bus arbitration)会对竞争作出裁决,这里我们假设总线在仲裁后判定处理器A在竞争中获胜(总线仲裁会确保所有处理器都能公平的访问内存)。此时处理器 A 继续它的总线事务,而其它两个处理器则要等待处理器A的总线事务完成后才能开始再次执行内存访问。假设在处理器 A 执行总线事务期间(不管这个总线事务是读事务还是写事务),处理器D向总线发起了总线事务,此时处理器 D 的这个请求会被总线禁止。
总线的这些工作机制可以把所有处理器对内存的访问以串行化的方式来执行;在任意时间点,最多只能有一个处理器能访问内存。这个特性确保了单个总线事务之中的内存读/写操作具有原子性。
在一些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 “写了一半“的无效值。
注意,在 JSR -133 之前的旧内存模型中,一个64位 long/ double 型变量的读/写操作可以被拆分为两个32位的读/写操作来执行。从 JSR -133 内存模型开始(即从 JDK5 开始),仅仅只允许把一个64位 long/ double 型变量的写操作拆分为两个32位的写操作来执行,任意的读操作在 JSR -133 中都必须具有原子性(即任意读操作必须要在单个读事务中执行)。
java内存模型-顺序一致性的更多相关文章
- Java内存模型深度解析:顺序一致性--转
原文地址:http://www.codeceo.com/article/java-memory-3.html 数据竞争与顺序一致性保证 当程序未正确同步时,就会存在数据竞争.java内存模型规范对数据 ...
- 深入理解Java内存模型(三)——顺序一致性
数据竞争与顺序一致性保证 当程序未正确同步时,就会存在数据竞争.java内存模型规范对数据竞争的定义如下: 在一个线程中写一个变量, 在另一个线程读同一个变量, 而且写和读没有通过同步来排序. 当代码 ...
- 【转】深入理解Java内存模型(三)——顺序一致性
数据竞争与顺序一致性保证 当程序未正确同步时,就会存在数据竞争.java内存模型规范对数据竞争的定义如下: 在一个线程中写一个变量, 在另一个线程读同一个变量, 而且写和读没有通过同步来排序. 当代码 ...
- 深入理解JMM(Java内存模型) --(三)顺序一致性
数据竞争与顺序一致性保证 当程序未正确同步时,就会存在数据竞争.Java内存模型规范对数据竞争的定义如下: 在一个线程中写一个变量, 在另一个线程读同一个变量, 而且写和读没有通过同步来排序. 当代码 ...
- Java内存模型(三)原子性、内存可见性、重排序、顺序一致性、volatile、锁、final
一.原子性 原子性操作指相应的操作是单一不可分割的操作.例如,对int变量count执行count++d操作就不是原子性操作.因为count++实际上可以分解为3个操作:(1)读取变量co ...
- Java内存模型深度解析:总结--转
原文地址:http://www.codeceo.com/article/java-memory-7.html 处理器内存模型 顺序一致性内存模型是一个理论参考模型,JMM和处理器内存模型在设计时通常会 ...
- java内存模型-总结
处理器内存模型 顺序一致性内存模型是一个理论参考模型,JMM 和处理器内存模型在设计时通常会把顺序一致性内存模型作为参照.JMM 和处理器内存模型在设计时会对顺序一致性模型做一些放松,因为如果完全按照 ...
- 深入理解Java内存模型(七)——总结
处理器内存模型 顺序一致性内存模型是一个理论参考模型,JMM和处理器内存模型在设计时通常会把顺序一致性内存模型作为参照.JMM和处理器内存模型在设计时会对顺序一致性模型做一些放松,因为如果完全按照顺序 ...
- 【转】深入理解Java内存模型(七)——总结
处理器内存模型 顺序一致性内存模型是一个理论参考模型,JMM和处理器内存模型在设计时通常会把顺序一致性内存模型作为参照.JMM和处理器内存模型在设计时会对顺序一致性模型做一些放松,因为如果完全按照顺序 ...
随机推荐
- NYOJ995硬币找零(简单dp)
/* 题意:给你不同面额的硬币(每种硬币无限多),需要找零的面值是T,用这些硬币进行找零, 如果T恰好能被找零,输出最少需要的硬币的数目!否则请输出剩下钱数最少的找零方案中的最少硬币数! 思路:转换成 ...
- maven -- 学习笔记(四)实现在Eclipse用maven搭建springmvc项目(附构建步骤和详细实现代码)
Learn from:http://www.cnblogs.com/fangjins/archive/2012/05/06/2485459.html,感谢楼主的分享,才有下面的这篇学习小结 一.环境准 ...
- Linux - 常用Shell命令
珠玉在前,不再赘言. 对Linux新手非常有用的20个命令 对中级Linux用户非常有用的20个命令 对Linux专家非常有用的20个命令
- Elasticsearch之_default_—— 为索引添加默认映射
前篇说过,ES可以自动为文档设定索引.但是问题也来了——如果默认设置的索引不是我们想要的,该怎么办呢? 要知道ES这种搜索引擎都是以Index为实际的分区,Index里面包含了不同的类型,不同的类型是 ...
- 如何做好一个ORM框架
很多人都不太认可以第三方ORM,因为考虑的点不够全面,没有用户群体大的ORM有保证,这点是不可否认确是事实. 但是往往用户群体大的ORM又有不足之处,就拿用户群体最多的两个ORM来说一下吧 1.EF ...
- linux专题一之文件描述符、重定向、管道符、tee命令
本节讨论一下几个问题: 1. 文件描述符. 2. 重定向. 3. 管道符 4. tee的用法. 1. 文件描述符. 在linux系统中一切皆文件.文件夹和设备都是文件.如何用来区别不同的文件呢?这里的 ...
- ES6笔记(4)-- Symbol类型
系列文章 -- ES6笔记系列 Symbol是什么?中文意思是标志.记号,顾名思义,它可以用了做记号. 是的,它是一种标记的方法,被ES6引入作为一种新的数据类型,表示独一无二的值. 由此,JS的数据 ...
- LINQ多个操作嵌套实现
获取集合,需要使用多个条件Where,排序OrderBy,查询Select一起. 先来分步实现: source code: string[] stringArray = { "hgdgh&q ...
- SQL去除回车符,换行符,空格和水平制表符
MS SQL去除回车符,换行符,空格和水平制表符,参考下面语句,一般情况是SQL接受富文本或是textarea的内容.在数据库接收到这些数据之后,还是对其做一些处理. ),),),''),' ','' ...
- javascript小数点后4舍5进
toFixed();获得小数点后面多少为数字.四舍五入的方法. 例如: var floatNum = 5.234 var temp = floatNum.toFixed(1); -- temp = 5 ...