从DCL的对象安全发布谈起(转载)
转载:《四火的唠叨》
http://www.raychase.net/1887
对于DCL(Double Check Lock)情况下的对象安全发布,一直理解得不足够清楚;在通过和同事,以及和互联网上一些朋友的讨论之后,我觉得已经把问题搞清楚了。我把我对这个问题的理解简要记录在这里。
现在有代码A:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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,变成如下情况呢?
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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的行为是乱序的(前面已经介绍过了,只需要保证结果正确即可,在这里就是保证getInstance方法返回的结果是正确的;但是,在getInstance方法内部,当instance不为空的时候,T的初始化行为却未必已经完成),这个就有可能取到一个没有初始化完成的残缺的对象。
除了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一致性。值得一提的是其中的第一条,需要理解其中的“subsequent action”(后续行为),比如调用一个方法返回的结果应当是正确的,类的每一条静态语句的执行结果也是正确的。这是hb内存模型在降低约束、提供更多优化可能的同时,必须要做到的正确性上的保证。
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修饰属性的情况下,才可以保证在对象初始化完成之后,外部能够看到对象正确的属性值。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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一致性保证(f可能被赋了一个构造并未完成的对象),其次对于属性x,由于final关键字的效应,f不为空的时候,f已经初始化完成,所以f.x一定为准确的3,但是f.y就不一定了。
还有其它的单例对象安全发布的方式:
|
1
2
3
4
5
6
7
8
|
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的方式:
|
1
2
3
4
5
6
7
8
9
10
11
|
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规范下载
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接《四火的唠叨》

浙公网安备 33010602011771号
<code>
classT {privatestaticT instance;publicstaticT getInstance() {if(null== instance) {synchronized(T.class) {if(null== instance) {instance =newT();}}}returninstance;}}</code>这种写法错误的原因是什么呢?synchronized的语义保证了在退出语句块时本地内存刷新
主内存。假设A线程内new T()是乱序的,但是B线程读取instance变量是主内存中的,应该还是null,因为A线程还没退出synchronize块,还没来得及回写主内存啊。
因为synchronized关键字,线程B检查发现instance确实不为空,所以就不进入同步块,这没有问题,然而,线程B得到的instance却未必是一个已经完全实例化了的T对象,换言之,并没有任何途径保证“new T()”happens-before“给instance的赋值”。
如果你还是无法理解,可以看看这篇文章 http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html帮助你理解
这个问题我理解了,是因为A线程的解锁操作和B线程对instance的第一次检查(同
步块之前的)之间没有happens-before关系,线程A虽然执行了刷回主内存的操作,
但是线程B无法保证能读到正确的。因为脱离了监视器之间的happens-before约束。
classT {privatestatic volatileT instance;publicstaticT getInstance() {if(null== instance) {synchronized(T.class) {if(null== instance) {instance =newT();}}}returninstance;}}如果是以上写法,是安全发布么?
instance =newT();这一句是不是相当于T temp = new T();instance = temp;呢?instance = new T();不是一个原子写操作,会有问题么?
是安全发布。就是因为volatile保证了“new T()”必定happens-before于“对instance的赋值”。
T temp=new T(); instance=temp; 这一小段确实不是原子操作,但是,因为instance有volatile修饰,那么无论赋值前怎么个执行顺序,都一定happens-before于给instance赋值这一步之前,因此是没有问题的。
为什么volatile能保证 new T()必定happens-before 于“对象instance的赋值”?volatile只是保证每次的读操作都能读到最后写的内容。写成T temp = new T();instance = temp;是因为可以利用Each action in a thread happens-before every subsequent action in that thread。再利用valotile的写happens-before读。可以参考http://www.infoq.com/cn/articles/java-memory-model-4。
final 怎么从jmm角度理解?没明白final是怎么保证可见性的。如果final的是一个对象呢?final保证引用不变,但如何保证属性值发生变化时的可见性呢?
请阅读规范,这是我的理解:
final假定你的对象是事实不可变的,并且保证在构造方法返回的时候,取得的是正确构造完成的对象(也包括这个对象内部的引用对外真实可见);但是这以后的时间通过调用final修饰的对象的方法,引起final对象内部的改变,规范里面并没有说明(我没有找到),因此是无法保证的。