定义:
顺序一致性是一种内存模型的理论参考模型,也就是他并不是实际存在的,处理器的内存模型和编程语言都会以它为参照。
一、数据竞争和顺序一致性
在一个线程中写一个变量,
在另一个线程读同一个变量,
而且写和读没有通过同步来排序。
当代码中包含数据竞争时,程序的执行往往产生违反直觉的结果(前一章的示例正是如 此)。如果一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序。
JMM对正确同步的多线程程序的内存一致性做了如下保证。
如果程序是正确同步的,程序的执行将具有顺序一致性(Sequentially Consistent)——即程 序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。马上我们就会看到,这对 于程序员来说是一个极强的保证。这里的同步是指广义上的同步,包括对常用同步语 (synchronized、volatile和final)的正确使用。
二、顺序一致性:
1.一个线程中所有的顺序都按照程序顺序执行
2、不管程序是否同步所有线程只能看作单一顺序执行,对所有其他线程都是可见的,
在概念上,顺序一致性是一个单一的全局内存,这个全局内存又一个可以连接任意线程开关,同时每个线程按照程序顺序执行读写操作,由上图可知在同一个时间点只能有一个线程连接内存,当多线程并发时,内存的开关可以将所有的操作串行话,(也就是在顺序一致模型中,所有的操作都具有全序关系)
就像是在A和B线程上添加监控器来正确同步,监控器A保证A1A2A3的顺序性,监控器B 保证B1B2B3顺序,而监控器A和B保证A和B的顺序。
如果两个线程没有同步的保证如下图
未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一 个一致的整体执行顺序。以上图为例,线程A和B看到的执行顺序都是: B1→A1→A2→B2→A3→B3。之所以能得到这个保证是因为顺序一致性内存模型中的每个操 作必须立即对任意线程可见。
但是,在JMM中就没有这个保证。未同步程序在JMM中不但整体的执行顺序是无序的,而 且所有线程看到的操作执行顺序也可能不一致。比如,在当前线程把写过的数据缓存在本地 内存中,在没有刷新到主内存之前,这个写操作仅对当前线程可见;从其他线程的角度来观 察,会认为这个写操作根本没有被当前线程执行。只有当前线程把本地内存中写过的数据刷 新到主内存之后,这个写操作才能对其他线程可见。在这种情况下,当前线程和其他线程看到 的操作执行顺序将不一致。
3.同步程序的顺序一致性效果
上图通过synchronized 实现了线程A调用writer 线程b调用reader的俩个监控器实现了一个正确同步的多线程。
在顺序一致性下所有操作都是串行执行的,而在jMM中临界区代码会发生重排(但jmm不许临界区代码溢出到临界区以外,那样会破坏监控器的语义)
JMM会在退出临界区和进入临界区这两个关键时间点做一些特别处理,使得线程在这两 个时间点具有与顺序一致性模型相同的内存视图(具体细节后文会说明)。
虽然线程A在临界 区内做了重排序,但由于监视器互斥执行的特性,这里的线程B根本无法“观察”到线程A在临 界区内的重排序。这种重排序既提高了执行效率,
又没有改变程序的执行结果。
从上图看到,JMM的方针就是,在不改变程序结果下(正确同步),尽可能为编译器和处理器的优化打开方便之门。
四、未同步的程序的特性
对于未同步或者不能正确同步多线程程序,jmm只能提供最小安全性,线程执行时读取的值要不是之前某个线程写入的,要么为默认值(null,0, flase),JMM保证线程读操作时读取的值不是无中生有的。为了保证最小安全性,所以每次JVM分配对象是,都会将对应内存空间进行清零,然后才会在上面分配对象(JVM会同步这两个步骤也就是一定是先删除后分配),在已经清零的内存空间分配对象时,域的默认初始化已经完成。
jmm不保证未同步的程序执行结果与该程序在顺序一致模型中的执行结果相同,如果想要保证执行执行结果一致,那么需要进行很多编译器和处理器的重排,这样对程序性能有一定的影响,而且最重要的问题是未同步的程序在顺序一致性模型中执行时,整体是无序的,所以执行结果也是不可知的,而且,保证未同步程序在这两个模型中的执行结果一致没什么意义。
未同步程序在JMM中的执行时,整体上是无序的,其执行结果无法预知。未同步程序在两 个模型中的执行特性有如下几个差异。
1.顺序一致性模型保证单线程内执行的顺序是和程序顺序相同,但是jmm不保证,会在临界区进行重排
2.顺序一致性模型保证所有线程看到的执行顺序都一支,但是jmm不能保证(虽然线程A在临界 区内做了重排序,但由于监视器互斥执行的特性,这里的线程B根本无法“观察”到线程A在临 界区内的重排序。这种重排序既提高了执行效率,又没有改变程序的执行结果。)
3.jmm不保证对64long和double变量写操作的原子性, 顺序一致性模型可以保证内存中读写都具有原子性。
为何JMM不能保证对long和double写的一致性:这个和处理器的的总线的工作原理有关,每次处理器和内存之间进行数据交互时都有一系列的操作,这一系列的操作叫做总线事务,总写事务有包括读事物和我写事务,读事务是将内存中的数据读到处理器中,写事务是将处理器的数据写到内存中,每个事务都会读/写一个或多个物理上面的字,总线总是会尝试同步并发使用总线的事务,
一个处理起执行总线事务时,总线会禁止其他处理器和io设备执行内存的读和写
同步上图知道,如果a b c d四个处理同时访问总线,然后a b c d 通过总线仲裁决定谁去抢占总线事务,a抢到以后其他处理器不能进行内存访问,要等到a完成才可以,如果这时f处理器来访问,那么会禁止他访问。
在32位处理器上,如果对一个64为的数要求一致性,开销会很大,所以java不强求jvm对double和long写的原子性,可以将它拆成两个32位的写操作,这两个写操做可能会分派到不同的总线事务中,这样就会使写操作不能保证原子性,如下图
但是在java5以后也就是JSR-133以后,只可以将写操作拆分为两个32位的,读操作只能是一个64位的,java5以前的话64的操作可以将读/写都拆分为两个32位操作,读也可以拆分,所以就出现了上面读32位的高位。