java并发编程(十六)happen-before规则
转载请注明出处:http://blog.csdn.net/ns_code/article/details/17348313
happen—before规则介绍
Java语言中有一个“先行发生”(happen—before)的规则,它是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其意思就是说,在发生操作B之前,操作A产生的影响都能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的先后发生基本没有太大关系。这个原则特别重要,它是判断数据是否存在竞争、线程是否安全的主要依据。
举例来说,假设存在如下三个线程,分别执行对应的操作:
---------------------------------------------------------------------------
线程A中执行如下操作:i=1
线程B中执行如下操作:j=i
线程C中执行如下操作:i=2
---------------------------------------------------------------------------
假设线程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的规则,它们无需任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随机地重排序。
1、程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作。
2、管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序,下同)对同一个锁的lock操作。
3、volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
4、线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
5、线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
6、线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
7、对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
8、传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。
时间上先后顺序和happen—before原则
”时间上执行的先后顺序“与”happen—before“之间有何不同呢?
1、首先来看操作A在时间上先与操作B发生,是否意味着操作A happen—before操作B?
一个常用来分析的例子如下:
private int value = ; public int get(){
return value;
}
public void set(int value){
this.value = value;
}
解决方法:可以将setValue(int)方法和getValue()方法均定义为synchronized方法,也可以把value定义为volatile变量(value的修改并不依赖value的原值,符合volatile的使用场景),分别对应happen—before规则的第2和第3条。注意,只将setValue(int)方法和getvalue()方法中的一个定义为synchronized方法是不行的,
2、其次来看,操作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,它们必定在时间上是完全错开的,这实际上也是同步的语义之一(独占访问)。
利用happen—before规则分析DCL
public class LazySingleton {
private int someField; private static LazySingleton instance; private LazySingleton() {
this.someField = new Random().nextInt()+; // (1)
} public static LazySingleton getInstance() {
if (instance == null) { // (2)
synchronized(LazySingleton.class) { // (3)
if (instance == null) { // (4)
instance = new LazySingleton(); // (5)
}
}
}
return instance; // (6)
} public int getSomeField() {
return this.someField; // (7)
}
}
前面我们说了,线程Ⅱ在执行语句(2)时也有可能观察空值,如果是种情况,那么它需要进入同步块,并执行语句(4)。在语句(4)处线程Ⅱ还能够读到instance的空值吗?不可能。这里因为这时对instance的写和读都是发生在同一个锁确定的同步块中,这时读到的数据是最新的数据。为也加深印象,我再用happen-before规则分析一遍。线程Ⅱ在语句(3)处会执行一个lock操作,而线程Ⅰ在语句(5)后会执行一个unlock操作,这两个操作都是针对同一个锁--Singleton.class,因此根据第2条happen-before规则,线程Ⅰ的unlock操作happen-before线程Ⅱ的lock操作,再利用单线程规则,线程Ⅰ的语句(5)
-> 线程Ⅰ的unlock操作,线程Ⅱ的lock操作 -> 线程Ⅱ的语句(4),再根据传递规则,就有线程Ⅰ的语句(5) ->
线程Ⅱ的语句(4),也就是说线程Ⅱ在执行语句(4)时能够观测到线程Ⅰ在语句(5)时对Singleton的写入值。接着对返回的instance调用getSomeField()方法时,我们也能得到线程Ⅰ的语句(1)
-> 线程Ⅱ的语句(7)(由于线程Ⅱ有进入synchronized块,根据规则2可得),这表明这时getSomeField能够得到正确的值。但是仅仅是这种情况的正确性并不妨碍DCL的不正确性,一个程序的正确性必须在所有的情况下的行为都是正确的,而不能有时正确,有时不正确。
对DCL的分析也告诉我们一条经验原则:对引用(包括对象引用和数组引用)的非同步访问,即使得到该引用的最新值,却并不能保证也能得到其成员变量(对数组而言就是每个数组元素)的最新值。
public class Singleton { private Singleton() {} // Lazy initialization holder class idiom for static fields
private static class InstanceHolder {
private static final Singleton instance = new Singleton();
} public static Singleton getSingleton() {
return InstanceHolder.instance;
}
}
这样我们便可以得到,线程Ⅰ的语句(5) -> 语线程Ⅱ的句(2),根据单线程规则,线程Ⅰ的语句(1) ->
线程Ⅰ的语句(5)和语线程Ⅱ的句(2) -> 语线程Ⅱ的句(7),再根据传递规则就有线程Ⅰ的语句(1) ->
语线程Ⅱ的句(7),这表示线程Ⅱ能够观察到线程Ⅰ在语句(1)时对someFiled的写入值,程序能够得到正确的行为。
注:
1、volatile屏蔽指令重排序的语义在JDK1.5中才被完全修复,此前的JDK中及时将变量声明为volatile,也仍然不能完全避免重排序所导致的问题(主要是volatile变量前后的代码仍然存在重排序问题),这点也是在JDK1.5之前的Java中无法安全使用DCL来实现单例模式的原因。
2、把volatile写和volatile读这两个操作综合起来看,在读线程B读一个volatile变量后,写线程A在写这个volatile变量之前,所有可见的共享变量的值都将立即变得对读线程B可见。
3、
在java5之前对final字段的同步语义和其它变量没有什么区别,在java5中,final变量一旦在构造函数中设置完成(前提是在构造函数中没有泄露this引用),其它线程必定会看到在构造函数中设置的值。而DCL的问题正好在于看到对象的成员变量的默认值,因此我们可以将LazySingleton的someField变量设置成final,这样在java5中就能够正确运行了。
java并发编程(十六)happen-before规则的更多相关文章
- Java并发编程(六)volatile关键字解析
由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识. 一.内存模型的相关概念 Java内存模型规定所有的变量都是存在 ...
- Java并发(十六):并发工具类——Exchanger
Exchanger(交换者)是一个用于线程间协作的工具类.Exchanger用于进行线程间的数据交换.它提供一个同步点,在这个同步点两个线程可以交换彼此的数据.这两个线程通过exchange方法交换数 ...
- java并发编程(六)----(JUC)Semaphore
Semaphore,从字面意义上我们知道他是信号量的意思.在java中,一个计数信号量维护了一个许可集.Semaphore 只对可用许可的号码进行计数,并采取相应的行动.拿到信号量的线程可以进入代码, ...
- Java并发编程(六)原子性与易变性
原子性 原子是最小单元.不可再分的意思.原子性是指某个操作在获取CPU时间时,要么就给它足够时间,让这个操作执行完,要么就不执行这个操作,执行时不能出现上下文切换(把CPU时间从一个线程分配到另一个线 ...
- Java并发编程 (十) 多线程并发拓展
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 一.死锁 1.死锁的定义 所谓的死锁是指两个或两个以上的线程在等待执行的过程中,因为竞争资源而造成的一种 ...
- Java并发编程(六)-- 同步块
上一节已经讲到,使用Synchronzied代码块可以解决共享对象的竞争问题,其实还有其他的方法也可以避免资源竞争问题,我统称他们为Java同步块.Java 同步块(synchronized bloc ...
- 《Java并发编程实战》笔记-Happens-Before规则
Happens-Before规则 程序顺序规则.如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前执行. 监视器锁规则.在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行. v ...
- Java并发编程(六):Java里实现对象安全发布的四种方式
接上篇,首先要了解什么是对象的发布与逸出? Java里安全发布对象的四种方法1.单例(注意懒汉和饿汉的区别)2.静态属性,注意类里的静态域和静态代码块的顺序有要求3.枚举4.final
- Java并发编程(六)发布与逸出
"发布(Publish)"一个对象的意思指,使对象能够在作用域之外的代码中使用. 例如: 将一个指向该对象的引用保存到其他代码可以访问的地方 在一个非私有的方法中返回该引用 将引用 ...
- java并发编程(六)Runnable和Thread实现多线程的区别
http://blog.csdn.net/ns_code/article/details/17161237
随机推荐
- 定义类型uint8_t,uint32_t
定义的类型uint8_t,uint32_t能更明显的显示所占字节数.uint8_t表示占1个字节(1 字节=8 bit), uint32_t表示占4个字节((4 字节=32 bit). #includ ...
- java Timer 定时每天凌晨1点执行任务
import java.util.TimerTask;/** * 执行内容 * @author admin_Hzw * */public class Task extends TimerTask { ...
- ProtocolBuffers-3 For Objective C (2)-进阶
先介绍几个常用关键字: equired前缀表示该字段为必要字段,既在序列化和反序列化之前该字段必须已经被赋值.与此同时,在Protocol Buffer中还存在另外两个类似的关键字,optional和 ...
- [Project Name] was compiled with optimization - stepping may behave oddly; variables may not be available.
控制台输出的时候显示一串这样的信息:[Project Name] was compiled with optimization - stepping may behave oddly; variabl ...
- Spring + Jedis集成Redis(集群redis数据库)
前段时间说过单例redis数据库的方法,但是生成环境一般不会使用,基本上都是集群redis数据库,所以这里说说集群redis的代码. 1.pom.xml引入jar <!--Redis--> ...
- brew 安装 mysql
在网上看到各种教程,都会出现ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.soc ...
- 网站开启https后加密协议始终是TLS1.0如何配置成TLS1.2?
p { margin-bottom: 0.1in; line-height: 120% } 网站开启https后加密协议始终是TLS1.0如何配置成TLS1.2? 要在服务器上开启 TLSv1.,通常 ...
- [Tool]Inno Setup创建软件安装程序。
这篇博客将介绍如何使用Inno Setup创建一个软件安装程序. Inno Setup官网:http://www.jrsoftware.org/isinfo.php. 可以下载到最新的Inno Set ...
- [Spring] AOP, Aspect实例解析
最近要用到切面来统一处理日志记录,写了个小实例练了练手: 具体实现类: public interface PersonServer { public void save(String name); p ...
- 前端知识杂烩(HTML[5]?+CSS篇)
1. CSS 优先级算法如何计算?2.如何居中div?如何居中一个浮动元素?如何让绝对定位的div居中?3.用纯CSS创建一个三角形的原理是什么?4. 如何解决inline-block元素的空白间距( ...