如果另一个线程要someObject
在“构造期间”检查变量,我相信它可能(由于内存模型中的怪癖)看到一个部分初始化的对象。新的(从 Java 5 开始)内存模型意味着任何final字段都应该在对象对其他线程可见之前设置为它们的值(只要对新创建对象的引用不会从任何其他线程的构造函数中逃逸)方式)但除此之外没有太多保证。
基本上,不要在没有适当锁定(或静态初始化器等提供的保证)的情况下共享数据:) 严重的是,内存模型非常棘手,一般来说无锁编程也是如此。尽量避免这种可能性。
从逻辑上讲,赋值发生在构造函数运行之后 - 因此,如果您从同一个线程观察变量,它将在构造函数调用期间为空。但是,正如我所说,内存模型存在一些奇怪之处。
编辑:出于双重检查锁定的目的,如果您的字段是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.1.13 节)到适当的标准值集(不是扩展指数值集),并将转换的结果存储到变量中。
然后从第 17.4.5 节:
两个动作可以通过happens-before关系排序。如果一个动作发生在另一个动作之前,那么第一个动作对第二个动作可见并在第二个动作之前排序。
如果我们有两个动作 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)。
应该注意的是,两个动作之间存在之前发生的关系并不一定意味着它们必须在实现中以该顺序发生。如果重新排序产生与合法执行一致的结果,则不是非法的。
换句话说,即使在单个线程中发生奇怪的事情也是可以的,但那一定是不可观察的。在这种情况下,差异是可以观察到的,这就是为什么我认为这是非法的。