从DCL的对象安全发布谈起

标签: Multi-thread & Asynchronism Recommended DCL final happens-before | 发表时间:2013-12-28 22:05 | 作者:四火
出处:http://www.raychase.net

文章系本人原创,转载请保持完整性并注明出自 《四火的唠叨》

从DCL的对象安全发布谈起

对于DCL(Double Check Lock)情况下的对象安全发布,一直理解得不足够清楚;在通过和同事,以及和互联网上一些朋友的讨论之后,我觉得已经把问题搞清楚了。我把我对这个问题的理解简要记录在这里。

现在有代码A:

class T {
	private static volatile T instance;
	public M m; // 这里没有final修饰

	public static T getInstance() {
		if (null == instance) {
			synchronized (T.class) {
				if (null == instance) {
					instance = new T();
					instance.m = new M();
				}
			}
		}
		return instance;
	}
}

以及代码B: 

class T {
	private static volatile T instance;
	public M m; // 这里没有final修饰

	public static T getInstance() {
		if (null == instance) {
			synchronized (T.class) {
				if (null == instance) {
					T temp = new T();
					temp.m = new M();
					instance = temp;
				}
			}
		}
		return instance;
	}
}

 

这两段代码是否做到了对象安全发布?

这里需要稍微解释一下,所谓对象安全发布,在这里可以这样理解,有一个线程X调用getInstance方法,第一次来获取对象,instance为空,这个时候进入同步块,初始化了instance并返回;这以后另一个线程Y也调用getInstance方法,不进入同步块了,获取到的instance对象是否一定是预期的——即对象的m属性不为空?如果是,表示对象被安全发布了,反之则不是。

happens-before一致性

仔细读了读JSR-133的规范文档,里面定义了happens-before(hb)一致性:

Happens-before consistency says that a read r of a variable v is allowed to observe a write w to v if, in the happens-before partial order of the execution trace:

  • r is not ordered before w (i.e., it is not the case that r hb w), and 
  • there is no intervening write w’ to v (i.e., no write w’ to v such that w hb w’ hb r).

这就是说,如果任何时候在满足以下这样两个条件的情况下,对一个对象的读操作r,都能得到对于对象的写操作w的结果(读的时候要能返回写的结果),我们就认为它就是满足happens-before一致性的:

  • 读必须不能发生在写操作之前;
  • 没有一个中间的写操作w’发生在w和r之间。

满足这样一致性的内存模型,是一种极度简化的内存模型,它允许JVM实现的时候,对于绝大多数情况下不需要满足happens-before的对象和操作,可以在保证单个线程运行的最终结果正确的情况下做尽可能多的优化,比如代码乱序执行,比如从主内存中缓存某些变量到寄存器等等。

volatile和happends-before的关系

A write to a volatile field happens-before every subsequent read of that volatile.

就是说,对于volatile修饰的属性的读写操作满足happens-before一致性。

再结合代码来看,代码A对于m的赋值发生在volatile修饰的instance之后,不能保证线程X中给instance的属性赋的值new M()能被线程Y看到;而代码B所有对于实例初始化的操作都放 instance=temp; (即对volatile修饰的属性instance的写操作)之前,这些操作的结果都是“可见的”。也就是说,代码A无法安全发布对象,但是代码B可以。

需要说明的是,如果对于代码B,干脆去掉属性m,但是也拿掉volatile,变成如下情况呢?

class T {
	private static T instance;

	public static T getInstance() {
		if (null == instance) {
			synchronized (T.class) {
				if (null == instance) {
					instance = new T();
				}
			}
		}
		return instance;
	}
}

这种情况下对象又无法安全发布了,因为没有了volatile的约束,对象初始化的行为和把对象赋给instance的行为是乱序的,这个就有可能取到一个没有初始化完成的残缺的对象。

除了volatile关键字以外,还有哪些情况下也满足happens-before一致性呢?

  • Each action in a thread happens-before every subsequent action in that thread.
  • An unlock on a monitor happens-before every subsequent lock on that monitor.
  • A write to a volatile field happens-before every subsequent read of that volatile.
  • A call to start() on a thread happens-before any actions in the started thread.
  • All actions in a thread happen-before any other thread successfully returns from a join() on that thread.
  • If an action a happens-before an action b, and b happens before an action c, then a happens- before c.

简单说,就是同一个线程,加锁,启动子线程,线程join()操作和满足传递性的三个操作这六种情况,其他所有的情况都不具备happens-before一致性。

final在JSR-133中的增强

由于final的值本身是不可被重写入的(所谓的“不变对象”),那么编译器就可以针对这一点进行优化:

Compilers are allowed to keep the value of a final field cached in a register and not reload it from memory in situations where a non-final field would have to be reloaded.

编译器可以把final修饰的属性的值缓存在寄存器里面,并且在执行过程中不重新加载它。

但是,如果对象属性不使用final修饰,在构造器调用完毕之后,其他线程未必能看到在构造器中给对象实例属性赋的真实值(除非有其他可行的方式保证happens-before一致性,比如前面提到的代码B):

A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object’s final fields.

仅当在使用final修饰属性的情况下,才可以保证在对象初始化完成之后,外部能够看到对象正确的属性值。

class FinalFieldExample {
	final int x;
	int y;
	static FinalFieldExample f;

	public FinalFieldExample() {
		x = 3;
		y = 4;
	}

	static void writer() {
		f = new FinalFieldExample();
	}

	static void reader() {
		if (f != null) {
			int i = f.x; // guaranteed to see 3
			int j = f.y; // could see 0
		}
	}
}

这个例子正式规范里面给出的,上面属性x使用了final修饰,而y没有,在某一时刻,一个线程调用writer()的时候,FinalFieldExample被初始化;之后另一个线程去取得这个对象,首先最开始的时候f未必一定不为空,因为f并没有任何happens-before一致性保证,其次对于属性x,由于final关键字的效应,f不为空的时候,f已经初始化完成,所以f.x一定为准确的3,但是f.y就不一定了。

还有其它的单例对象安全发布的方式:

public class T {
  private static final T instance = new T(); // final可少吗?
  public final M m = new M(); // final可少吗?

  public static T getInstance() {
    return instance;
  }
}

这种是很常见的,还有一种叫做Initialization On Demand Holder的方式:

class T {
	public final M m = new M(); // final可少吗?

	private static class LazyHolder {
		public static T instance = new T();
	}

	public static T getInstance() {
		return LazyHolder.instance;
	}
}

这两段代码在不使用的时候都可以保证对象安全发布的,因为这种写法下,对于属性的初始化会在对象的构造器调用前完成,这是前面说的happens-before的第一种(Each action in a thread happens-before every subsequent action in that thread.),属于对程序正确性的要求。

附件: JSR-133规范下载

文章系本人原创,转载请保持完整性并注明出自 《四火的唠叨》

分享到:
你可能也喜欢:

相关 [dcl 对象 安全] 推荐:

从DCL的对象安全发布谈起

- - 四火的唠叨
文章系本人原创,转载请保持完整性并注明出自 《四火的唠叨》. 对于DCL(Double Check Lock)情况下的对象安全发布,一直理解得不足够清楚;在通过和同事,以及和互联网上一些朋友的讨论之后,我觉得已经把问题搞清楚了. 我把我对这个问题的理解简要记录在这里. public M m; // 这里没有final修饰.

javascript对象转json

- - JavaScript - Web前端 - ITeye博客
把javascript对象转成json. 已有 0 人发表留言,猛击->> 这里<<-参与讨论. —软件人才免语言低担保 赴美带薪读研.

对象的消息模型

- loudly - 酷壳 - CoolShell.cn
[ ———— 感谢 Todd 同学 投递本文,原文链接 ———— ]. 话题从下面这段C++程序说起,你认为它可以顺利执行吗. 试试的确可以顺利运行输出hello world,奇怪吗. 其实并不奇怪,根据C++对象模型,类的非虚方法并不会存在于对象内存布局中,实际上编译器是把Hello方法转化成了类似这样的全局函数:.

jQuery的deferred对象详解

- 郑小东 - 阮一峰的网络日志
jQuery的开发速度很快,几乎每半年一个大版本,每两个月一个小版本. 今天我想介绍的,就是从jQuery 1.5.0版本开始引入的一个新功能----deferred对象. 这个功能很重要,未来将成为jQuery的核心方法,它彻底改变了如何在jQuery中使用ajax. 为了实现它,jQuery的全部ajax代码都被改写了.

我连对象都没有。。。

- 老五 - Lzhi&#39;s Views
两个黄鹂鸣翠柳,我连对象都没有. 劝君更尽一杯酒,我连对象都没有. 莫愁前路无知己,我连对象都没有. 借问酒家何处有,我连对象都没有. 停车坐爱枫林晚,我连对象都没有. 一枝红杏出墙来,我连对象都没有. 壮士一去不复还,我连对象都没有. 烈火焚烧浑不怕,我连对象都没有. 雌雄双兔奔地走,我连对象都没有.

js对象深拷贝

- - ITeye博客
在做一个前台页面你的时候用到了一个自己写的List对象,在进行深拷贝的时候参考了网上的代码:. //对象扩展,tObj被扩展对象,sObj扩展对象. Object.extend(a,b);//a获得了b的所有属性. 我自己定义的list中没有定义constructor,所以执行sObj.constructor == Array会报错,我就修改为:.

Java的对象驻留

- - Java译站
Java会将源代码中的字符串常量存储到常量池中. 这不只是说它俩的值是一样的,而是说就是同一个字符串对象. 用Java的话来说就是a==b的结果是true. 然而这个只对字符串以及小的整型或者长整型有效. 其它的对象是不会被驻留的,也就是说如果你创建了两个对象而他们的值是相等的,但他们并不是同一个对象.

方案对象管理

- - CSDN博客数据库推荐文章
方案是数据库用户拥有的数据库对象的集合,方案对象是直接引用数据库的逻辑结构,对象包括表、索引、序列、视图、同义词等结构.  这一章大部分内容,我们在SQL的第九章创建和管理表及约束和第十章其它常用对象都已讲过,做一些补充. 堆表就是普通表,也叫堆组织表. 之所以这样叫,是因为他使用数据结构中堆的算法来组织表.

jquery获取对象大全

- - CSDN博客Web前端推荐文章
1、JQuery的核心的一些方法 . $("Element").length; ‘元素的个数,是个属性 . $("Element").size(); ’也是元素的个数,不过带括号是个方法 . $("Element").get(); ‘某个元素在页面中的集合,以数组的形式存储 . $("Element").get(index); ’功能和上面的相同,index表示第几个元素,数组的下标 .

Hibrernate 数据对象状态

- - ITeye博客
Hibernate 中数据对象状态概念介绍. 一、 数据对象在Session中的状态:. 1、理解Session的缓存:Session接口是Hibernate向应用程序. 提供操作数据库的主要接口,它提供了基本的增、删、改、查 方法;. Session具有一个缓存,Hibernate的缓存包括Session的缓存和SessionFactory的缓存;.