领域 架构 & 设计, 语言 & 开发 主题 Java , 多线程 , 并发 , 内存模型 , 专栏 html
当程序未正确同步时,就会存在数据竞争。java内存模型规范对数据竞争的定义以下: java
当代码中包含数据竞争时,程序的执行每每产生违反直觉的结果(前一章的示例正是如此)。若是一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序。 程序员
JMM对正确同步的多线程程序的内存一致性作了以下保证: 编程
顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性: 缓存
顺序一致性内存模型为程序员提供的视图以下: 安全
在概念上,顺序一致性模型有一个单一的全局内存,这个内存经过一个左右摆动的开关能够链接到任意一个线程。同时,每个线程必须按程序的顺序来执行内存读/写操做。从上图咱们能够看出,在任意时间点最多只能有一个线程能够链接到内存。当多个线程并发执行时,图中的开关装置能把全部线程的全部内存读/写操做串行化。 多线程
为了更好的理解,下面咱们经过两个示意图来对顺序一致性模型的特性作进一步的说明。 架构
假设有两个线程A和B并发执行。其中A线程有三个操做,它们在程序中的顺序是:A1->A2->A3。B线程也有三个操做,它们在程序中的顺序是:B1->B2->B3。 并发
假设这两个线程使用监视器来正确同步:A线程的三个操做执行后释放监视器,随后B线程获取同一个监视器。那么程序在顺序一致性模型中的执行效果将以下图所示: oracle
如今咱们再假设这两个线程没有作同步,下面是这个未同步程序在顺序一致性模型中的执行示意图:
未同步程序在顺序一致性模型中虽然总体执行顺序是无序的,但全部线程都只能看到一个一致的总体执行顺序。以上图为例,线程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中的执行时,总体上也是无序的,其执行结果也没法预知。同时,未同步程序在这两个模型中的执行特性有下面几个差别:
第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位的读操做被拆分为两个32位的读操做,且这两个32位的读操做被分配到同一个的读事务中执行。当处理器A和B按上图的时序来执行时,处理器B将看到仅仅被处理器A“写了一半“的无效值。
程晓明,Java软件工程师,国家认证的系统分析师、信息项目管理师。专一于并发编程,就任于富士通南大。我的邮箱:asst2003@163.com。