Java happen-before规则缓存
synchronized、大部分锁,众所周知的一个功能就是使多个线程互斥/串行的(共享锁容许多个线程同时访问,如读锁)访问临界区,但他们的第二个功能 —— 保证变量的可见性 —— 常被遗忘。安全
为何存在可见性问题?简单介绍下。相对于内存,CPU的速度是极高的,若是CPU须要存取数据时都直接与内存打交道,在存取过程当中,CPU将一直空闲,这是一种极大的浪费,妈妈说,浪费是很差的,因此,现代的CPU里都有不少寄存器,多级cache,他们比内存的存取速度高多了。某个线程执行时,内存中的一份数据,会存在于该线程的工做存储中(working memory,是cache和寄存器的一个抽象,这个解释源于《Concurrent Programming in Java: Design Principles and Patterns, Second Edition》§2.2.7,原文:Every thread is defined to have a working memory (an abstraction of caches and registers) in which to store values. 有很多人以为working memory是内存的某个部分,这多是有些译做将working memory译为工做内存的缘故,为避免混淆,这里称其为工做存储,每一个线程都有本身的工做存储),并在某个特定时候回写到内存。单线程时,这没有问题,若是是多线程要同时访问同一个变量呢?内存中一个变量会存在于多个工做存储中,线程1修改了变量a的值何时对线程2可见?此外,编译器或运行时为了效率能够在容许的时候对指令进行重排序,重排序后的执行顺序就与代码不一致了,这样线程2读取某个变量的时候线程1可能尚未进行写入操做呢,虽然代码顺序上写操做是在前面的。这就是可见性问题的由来。多线程
咱们没法枚举全部的场景来规定某个线程修改的变量什么时候对另外一个线程可见。但能够制定一些通用的规则,这就是happens-before。app
“先行发生”(happen—before)的规则,它是Java内存模型中定义的两项操做之间的偏序关系,若是操做A先行发生于操做B,其意思就是说,在发生操做B以前,操做A产生的影响都能被操做B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的前后发生基本没有太大关系。这个原则特别重要,它是判断数据是否存在竞争、线程是否安全的主要依据。函数
举例来讲,假设存在以下三个线程,分别执行对应的操做:性能
---------------------------------------------------------------------------this
线程A中执行以下操做:i=1编码
线程B中执行以下操做:j=ispa
线程C中执行以下操做:i=2.net
---------------------------------------------------------------------------
假设线程A中的操做"i=1" happen—before线程B中的操做"j=i",那么就能够保证在线程B的操做执行后,变量j的值必定为1,即线程B观察到了线程A中操做"i=1"所产生的影响;如今,咱们依然保持线程A和线程B之间的happen—before关系,同时线程C出如今了线程A和线程B的操做之间,可是C与B并无happen—before关系,那么j的值就不肯定了,线程C对变量i的影响可能会被线程B观察到,也可能不会,这时线程B就存在读取到不是最新数据的风险,不具有线程安全性。
下面是Java内存模型中的八条可保证happen—before的规则,它们无需任何同步器协助就已经存在,能够在编码中直接使用。若是两个操做之间的关系不在此列,而且没法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机能够对它们进行随机地重排序。
一、程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操做happen—before(时间上)后执行的操做。
二、管理锁定规则:一个unlock操做happen—before后面(时间上的前后顺序,下同)对同一个锁的lock操做。
三、volatile变量规则:对一个volatile变量的写操做happen—before后面对该变量的读操做。
四、线程启动规则:Thread对象的start()方法happen—before此线程的每个动做。
五、线程终止规则:线程的全部操做都happen—before对此线程的终止检测,能够经过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
六、线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
七、对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
八、传递性:若是操做A happen—before操做B,操做B happen—before操做C,那么能够得出A happen—before操做C。
”时间上执行的前后顺序“与”happen—before“之间有何不一样呢?
一、首先来看操做A在时间上先与操做B发生,是否意味着操做A happen—before操做B?
一个经常使用来分析的例子以下:
private int value = 0; public int get(){ return value; } public void set(int value){ this.value = value; }
假设存在线程A和线程B,线程A先(时间上的先)调用了setValue(3)操做,而后(时间上的后)线程B调用了同一对象的getValue()方法,那么线程B获得的返回值必定是3吗?
对照以上八条happen—before规则,发现没有一条规则适合于这里的value变量,从而咱们能够断定线程A中的setValue(3)操做与线程B中的getValue()操做不存在happen—before关系。所以,尽管线程A的setValue(3)在操做时间上先于操做B的getvalue(),但没法保证线程B的getValue()操做必定观察到了线程A的setValue(3)操做所产生的结果,也便是getValue()的返回值不必定为3(有多是以前setValue所设置的值)。这里的操做不是线程安全的。
所以,“一个操做时间上先发生于另外一个操做”并不表明“一个操做happen—before另外一个操做”。
解决方法:能够将setValue(int)方法和getValue()方法均定义为synchronized方法,也能够把value定义为volatile变量(value的修改并不依赖value的原值,符合volatile的使用场景),分别对应happen—before规则的第2和第3条。注意,只将setValue(int)方法和getvalue()方法中的一个定义为synchronized方法是不行的,必须对同一个变量的全部读写同步,才能保证不读取到陈旧的数据,仅仅同步读或写是不够的 。
二、其次来看,操做A happen—before操做B,是否意味着操做A在时间上先与操做B发生?
x = 1; y = 2;
假设同一个线程执行上面两个操做:操做A:x=1和操做B:y=2。根据happen—before规则的第1条,操做A happen—before 操做B,可是因为编译器的指令重排序(Java语言规范规定了JVM线程内部维持顺序化语义,也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致。这个过程经过叫作指令的重排序。指令重排序存在的意义在于:JVM可以根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的从新排序机器指令,使机器指令更符合CPU的执行特色,最大限度的发挥机器的性能。在没有同步的状况下,编译器、处理器以及运行时等均可能对操做的执行顺序进行一些意想不到的调整)等缘由,操做A在时间上有可能后于操做B被处理器执行,但这并不影响happen—before原则的正确性。
所以,“一个操做happen—before另外一个操做”并不表明“一个操做时间上先发生于另外一个操做”。
最后,一个操做和另外一个操做一定存在某个顺序,要么一个操做或者是先于或者是后于另外一个操做,或者与两个操做同时发生。同时发生是彻底可能存在的,特别是在多CPU的状况下。而两个操做之间却可能没有happen-before关系,也就是说有可能发生这样的状况,操做A不happen-before操做B,操做B也不happen-before操做A,用数学上的术语happen-before关系是个偏序关系。两个存在happen-before关系的操做不可能同时发生,一个操做A happen-before操做B,它们一定在时间上是彻底错开的,这实际上也是同步的语义之一(独占访问)。
参考:http://ifeve.com/easy-happens-before/
http://blog.csdn.net/ns_code/article/details/17348313
=============END=============