假设你正在创建一个java对象:
SomeClass someObject = null; someObject = new SomeClass();
someObject在什么时候变为非null?是在SomeClass()
构造函数运行之前还是之后?
为了澄清一点,假设另一个线程是否someObject
在SomeClass()
构造函数完成一半时检查是否为null ,它是null还是非null?
另外,如果someObject
像这样创建会有什么区别:
SomeClass someObject = new SomeClass();
会someObject
永远是空的吗?
如果另一个线程要检查someObject
变量"在"构造期间,我相信它可能(由于内存模型中的怪癖)看到一个部分初始化的对象.新的(从Java 5开始)内存模型意味着在对象对其他线程可见之前,任何最终字段都应该设置为它们的值(只要对新创建的对象的引用不会从任何其他线程的构造函数中逃脱方式)但除此之外没有太多保证.
基本上,没有适当的锁定(或静态的inializers给出的保证等),不要共享数据:)严重的是,内存模型非常棘手,一般情况下无锁编程也是如此.尽量避免这种可能性.
在逻辑术语中,赋值发生在构造函数运行之后 - 因此,如果从同一个线程观察变量,它将在构造函数调用期间为null.但是,正如我所说,存在奇怪的记忆模型.
编辑:出于双重检查锁定的目的,如果您的字段是volatile
,并且您使用的是Java 5或更高版本,则可以使用此方法.在Java 5之前,内存模型不够强大.你需要完全正确地获得模式.有关详细信息,请参阅Effective Java,第2版,第71项.
编辑:这是我的理由,反对Aaron的内联在一个线程中可见.假设我们有:
public class FooHolder { public static Foo f = null; public static void main(String[] args) { f = new Foo(); System.out.println(f.fWasNull); } } // Make this nested if you like, I don't believe it affects the reasoning public class Foo { public boolean fWasNull; public Foo() { fWasNull = FooHolder.f == null; } }
我相信这总会报道true
.从第15.26.1节:
否则,需要三个步骤:
首先,评估左侧操作数以产生变量.如果此评估突然完成,则赋值表达式出于同样的原因突然完成; 不评估右侧操作数,也不进行赋值.
否则,将评估右侧操作数.如果此评估突然完成,则赋值表达式会出于同样的原因突然完成,并且不会发生任何分配.
否则,将右侧操作数的值转换为左侧变量的类型,进行值集转换(第5.113节)到相应的标准值集(不是扩展指数值集),并且转换的结果存储在变量中.
然后从第17.4.5节:
可以通过先发生关系来排序两个动作.如果一个动作发生在另一个动作之前,则第一个动作在第二个动作之前可见并在第二个之前被命
如果我们有两个动作x和y,我们写hb(x,y)来表示x发生在y之前.
如果x和y是同一个线程的动作,并且x在程序顺序中出现在y之前,那么hb(x,y).
从对象的构造函数的末尾到该对象的终结器(第12.6节)的开始有一个发生前的边缘.
如果动作x与后续动作y同步,那么我们也有hb(x,y).
如果是hb(x,y)和hb(y,z),那么hb(x,z).
应该注意的是,两个动作之间存在的先发生关系并不一定意味着它们必须在实现中以该顺序发生.如果重新排序产生的结果与合法执行一致,则不是非法的.
换句话说,即使在单个线程中也可能发生奇怪的事情,但这不可能是可观察的.在这种情况下,差异将是可观察的,这就是为什么我认为这是非法的.
someObject
null
在施工期间的某些时候将变为非.通常,有两种情况:
优化器已内联构造函数
构造函数未内联.
在第一种情况下,VM将执行此代码(伪代码):
someObject = malloc(SomeClass.size); someObject.field = ... ....
所以在这种情况下,someObject
不是null
并且它指向未100%初始化的内存,即并非所有构造函数代码都已运行!这就是双重检查锁定不起作用的原因.
在第二种情况下,构造函数中的代码将运行,引用将被传回(就像在普通方法调用中一样)并且someObject将在所有和每个init代码运行之后被设置为引用的值.
问题是没有办法告诉java不要提前分配someObject
.例如,您可以尝试:
SomeClass tmp = new SomeClass(); someObject = tmp;
但由于未使用tmp,优化器可以忽略它,因此它将生成与上面相同的代码.
所以这种行为是为了让优化器生成更快的代码,但是在编写多线程代码时它可能会咬你一口气.在单线程代码中,这通常不是问题,因为在构造函数完成之前不会执行任何代码.
[编辑]这是一篇很好的文章,解释了正在发生的事情:http://www.ibm.com/developerworks/java/library/j-dcl.html
PS:Joshua Bloch 撰写的" Effective Java,Second Edition "一书包含了Java 5及更高版本的解决方案:
private volatile SomeClass field; public SomeClass getField () { SomeClass result = field; if (result == null) { // First check, no locking synchronized(this) { result = field; if (result == null) { // second check with locking field = result = new SomeClass (); } } } return result; }
看起来很奇怪,但应该适用于每个Java VM.请注意,每一位都很重要; 如果省略双重赋值,则会导致性能不佳或部分初始化对象.如需完整说明,请购买本书.