Java 对象在构造期间何时变为非空?

2022-09-03 01:17:47

假设您正在创建一个Java对象,如下所示:

SomeClass someObject = null;
someObject = new SomeClass();

在什么时候 someObject 会变为非空值?是在构造函数运行之前还是在之后?SomeClass()

为了澄清一下,假设另一个线程要在构造函数完成一半时检查是否为空,那么它是空还是非空?someObjectSomeClass()

另外,如果像这样创建,会有什么区别:someObject

SomeClass someObject = new SomeClass();

会是空的吗?someObject


答案 1

如果另一个线程要在“期间”构造期间检查变量,我相信它可能会(由于内存模型中的怪癖)看到一个部分初始化的对象。新的(从Java 5开始)内存模型意味着,在对象对其他线程可见之前,任何最终字段都应该设置为它们的值(只要对新创建对象的引用不会以任何其他方式从构造函数中转义),但除此之外没有太多保证。someObject

基本上,如果没有适当的锁定(或静态初始化器等提供的保证),请不要共享数据:)说真的,内存模型非常棘手,一般的无锁编程也是如此。尽量避免这种情况成为一种可能性。

逻辑上讲,赋值发生在构造函数运行之后 - 因此,如果您从同一线程观察变量,则在构造函数调用期间它将为 null。但是,正如我所说,内存模型有些奇怪。

编辑:出于双重检查锁定的目的,如果您的字段是并且如果您使用的是Java 5或更高版本,则可以解决此问题。在Java 5之前,内存模型还不够强大。不过,您需要获得完全正确的模式。有关更多详细信息,请参阅有效 Java,第 2 版,第 71 项。volatile

编辑:这是我反对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;
    }
}

我相信这将永远报告。从第15.26.1节开始:true

否则,需要执行三个步骤:

  • 首先,计算左侧操作数以生成变量。如果此计算突然完成,则赋值表达式由于同样的原因突然完成;不计算右侧操作数,并且不会进行赋值。
  • 否则,将计算右侧操作数。如果此计算突然完成,则赋值表达式由于同样的原因突然完成,并且不会发生赋值。
否则,右侧操作数的值将转换为左侧变量的类型,进行值集转换 (§5.1.13) 到适当的标准值集(不是扩展指数值集),并将转换结果存储到变量中。

然后从第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)。

应该注意的是,两个操作之间存在“发生之前”关系并不一定意味着它们在实现中必须按该顺序发生。如果重新排序产生与合法执行一致的结果,则不违法。

换句话说,即使在单个线程中发生奇怪的事情也是可以的,但这一定是不可观察的。在这种情况下,差异是可以观察到的,这就是为什么我认为它是非法的。


答案 2

someObject在施工过程中的某个时刻将变得非- 。通常,有两种情况:null

  1. 优化程序已内联构造函数
  2. 构造函数未内联。

在第一种情况下,VM 将执行以下代码(伪代码):

someObject = malloc(SomeClass.size);
someObject.field = ...
....

因此,在这种情况下,不是它指向未100%初始化的内存,即并非所有构造函数代码都已运行!这就是双重检查锁定不起作用的原因。someObjectnull

在第二种情况下,来自构造函数的代码将运行,引用将被传递回去(就像在普通方法调用中一样),并且在所有和每个初始化代码都运行之后,someObject 将被设置为引用的值。

问题是没有办法告诉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上工作。请注意,每个位都很重要;如果省略双重赋值,则性能不佳或对象部分初始化。有关完整说明,请购买该书。