咱们知道Java 中volatile实现了修饰变量的原子性以及可见性,而且为了实现多线程环境下的线程安全,禁止了指令重排。程序员
首先咱们先来了解一下happens-before原则、as-if-serial语义以及数据依赖性,引用自《Java并发编程的艺术》编程
从JDK 5开始,Java使用新的JSR-133内存模型(除非特别说明,本文针对的都是JSR-133内存模型)。JSR-133使用happens-before的概念来阐述操做之间的内存可见性。在JMM中,若是一个操做执行的结果须要对另外一个操做可见,那么这两个操做之间必需要存在happens-before关系。这里提到的两个操做既能够是在一个线程以内,也能够是在不一样线程之间。缓存
与程序员密切相关的happens-before规则以下。安全
·程序顺序规则:一个线程中的每一个操做,happens-before于该线程中的任意后续操做。多线程
·监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。并发
·volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。app
·传递性:若是A happens-before B,且B happens-before C,那么A happens-before C。jvm
注意 两个操做之间具备happens-before关系,并不意味着前一个操做必需要在后一个优化
操做以前执行!happens-before仅仅要求前一个操做(执行的结果)对后一个操做可见,且前一个操做按顺序排在第二个操做以前(the first is visible to and ordered before the second)。spa
-----------------------------------------------------------------------------------------------------------------------------------------
as-if-serial语义的意思是:无论怎么重排序(编译器和处理器为了提升并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵照as-if-serial语义。
为了遵照as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操做作重排序,由于这种重排序会改变执行结果。可是,若是操做之间不存在数据依赖关系,这些操做就可能被编译器和处理器重排序。
-----------------------------------------------------------------------------------------------------------------------------------------
若是两个操做访问同一个变量,且这两个操做中有一个为写操做,此时这两个操做之间就存在数据依赖性。数据依赖分为下列3种类型,如表所示。
上面3种状况,只要重排序两个操做的执行顺序,程序的执行结果就会被改变。
编译器和处理器可能会对操做作重排序。编译器和处理器在重排序时,会遵照数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操做的执行顺序 。
这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操做,不一样处理器之间和不一样线程之间的数据依赖性不被编译器和处理器考虑。
-----------------------------------------------------------------------------------------------------------------------------------------
根据上面的一些描述,咱们或多或少的理解了一些事情:
上面的状况说明了咱们的程序在多线程的环境下运行,每次结果可能不相同,也就是线程不安全。
如今让咱们来看看,重排序是否会改变多线程程序的执行结果。请看下面的示例代码。
flag变量是个标记,用来标识变量a是否已被写入。这里假设有两个线程A和B,A首先执行writer()方法,随后B线程接着执行reader()方法。线程B在执行操做4时,可否看到线程A在操做1对共享变量a的写入呢?
答案是:不必定能看到。
因为操做1和操做2没有数据依赖关系,编译器和处理器能够对这两个操做重排序;一样,操做3和操做4没有数据依赖关系,编译器和处理器也能够对这两个操做重排序。让咱们先来看看,当操做1和操做2重排序时,可能会产生什么效果?请看下面的程序执行时序图,如图所示。
如图所示,操做1和操做2作了重排序。程序执行时,线程A首先写标记变量flag,随后线程B读这个变量。因为条件判断为真,线程B将读取变量a。此时,变量a尚未被线程A写入,在这里多线程程序的语义被重排序破坏了!
注意 本文统一用虚箭线标识错误的读操做,用实箭线标识正确的读操做。
下面再让咱们看看,当操做3和操做4重排序时会产生什么效果(借助这个重排序,能够顺便说明控制依赖性)。下面是操做3和操做4重排序后,程序执行的时序图,如图所示。
在程序中,操做3和操做4存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜想(Speculation)执行来克服控制相关性对并行度的影响。以处理器的猜想执行为例,执行线程B的处理器能够提早读取并计算a*a,而后把计算结果临时保存到一个名为重排序缓冲(Reorder Buffer,ROB)的硬件缓存中。当操做3的条件判断为真时,就把该计算结果写入变量i中。
从图中咱们能够看出,猜想执行实质上对操做3和4作了重排序。重排序在这里破坏了多线程程序的语义!
在单线程程序中,对存在控制依赖的操做重排序,不会改变执行结果(这也是as-if-serial语义容许对存在控制依赖的操做作重排序的缘由);但在多线程程序中,对存在控制依赖的操做重排序,可能会改变程序的执行结果。
那么volatile是如何作到禁止指令重排的呢?
jvm经过内存屏障来禁止指令重排,内存屏障类型表以下
StoreLoad Barriers是一个“全能型”的屏障,它同时具备其余3个屏障的效果。现代的多处理器大多支持该屏障(其余类型的屏障不必定被全部处理器支持)。执行该屏障开销会很昂贵,由于当前处理器一般要把写缓冲区中的数据所有刷新到内存中(Buffer Fully Flush)。
下面来看看JMM如何实现volatile写/读的内存语义。
前文提到太重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型。表3-5是JMM针对编译器制定的volatile重排序规则表。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来讲,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM采起保守策略。下面是基于保守策略的JMM内存屏障插入策略。
上述内存屏障插入策略很是保守,但它能够保证在任意处理器平台,任意的程序中都能获得正确的volatile内存语义。
下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图,如图所示。
图中的StoreStore屏障能够保证在volatile写以前,其前面的全部普通写操做已经对任意处理器可见了。这是由于StoreStore屏障将保障上面全部的普通写在volatile写以前刷新到主内存。
这里比较有意思的是,volatile写后面的StoreLoad屏障。此屏障的做用是避免volatile写与后面可能有的volatile读/写操做重排序。由于编译器经常没法准确判断在一个volatile写的后面是否须要插入一个StoreLoad屏障(好比,一个volatile写以后方法当即return)。为了保证能正确实现volatile的内存语义,JMM在采起了保守策略:在每一个volatile写的后面,或者在每一个volatile读的前面插入一个StoreLoad屏障。从总体执行效率的角度考虑,JMM最终选择了在每一个volatile写的后面插入一个StoreLoad屏障。由于volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写以后插入StoreLoad屏障将带来可观的执行效率的提高。从这里能够看到JMM在实现上的一个特色:首先确保正确性,而后再去追求执行效率。
下面是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图,如图所示。
图中的LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
编译器不会对volatile读与volatile读后面的任意内存操做重排序;编译器不会对volatile写与volatile写前面的任意内存操做重排序。
上述volatile写和volatile读的内存屏障插入策略很是保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器能够根据具体状况省略没必要要的屏障。下面经过具体的示例代码进行说明。
针对readAndWrite()方法,编译器在生成字节码时能够作以下的优化。
注意,最后的StoreLoad屏障不能省略。由于第二个volatile写以后,方法当即return。此时编译器可能没法准确判定后面是否会有volatile读或写,为了安全起见,编译器一般会在这里插入一个StoreLoad屏障。上面的优化针对任意处理器平台,因为不一样的处理器有不一样“松紧度”的处理器内存模型,内存屏障的插入还能够根据具体的处理器内存模型继续优化。
有了这些内存屏障的保证,volatile继而实现了禁止指令重排序。
参考:
《Java并发编程的艺术》