java 对象在构造过程中何时变为非空?

Posted

技术标签:

【中文标题】java 对象在构造过程中何时变为非空?【英文标题】:When does a java object become non-null during construction? 【发布时间】:2010-10-15 11:26:41 【问题描述】:

假设你正在像这样创建一个 java 对象:

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

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

稍微澄清一下,假设另一个线程要检查 someObject 是否为 null 而 SomeClass() 构造函数完成一半,它是 null 还是非 null?

另外,如果someObject 是这样创建的,会有什么不同:

SomeClass someObject = new SomeClass();

someObject 会不会为空?

【问题讨论】:

(如果是局部变量,在构造函数完成之前是无法观察到的。) 它可以是实例变量或静态成员。 【参考方案1】:

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

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

逻辑术语中,赋值发生在构造函数运行之后 - 所以如果你观察变量来自同一个线程,它将在期间为空构造函数调用。但是,正如我所说,内存模型有些奇怪。

编辑:出于双重检查锁定的目的,您可以摆脱这个 if 您的字段是 volatile 并且 if 您正在使用 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。来自section 15.26.1:

否则,需要三个步骤:

首先,计算左侧操作数以生成变量。如果此评估完成 突然,然后分配 表达式突然完成 同样的原因;右手操作数是 未评估且未分配 发生。 否则,将计算右侧操作数。如果这 评估突然完成,然后 赋值表达式完成 突然出于同样的原因,没有 分配发生。 否则,右手边的值 操作数转换为的类型 左边的变量,受到 到值集转换(§5.1.13)到 适当的标准值集 (不是扩展指数值集), 转换的结果是 存储到变量中。

然后来自section 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)。

需要注意的是,两者之间存在发生前的关系 行动并不一定意味着它们必须以这种顺序发生 执行。如果重新排序产生与合法执行一致的结果, 这并不违法。

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

【讨论】:

什么样的怪癖(我不太懂Java)?我很想知道什么会允许这样的事情,因为您会认为由于构造函数尚未完成运行,因此对变量的赋值还没有发生,这意味着变量仍然为空。 是的,那里没有“之前发生”。 final 字段有一些额外的保证。一般来说,不要编写带有种族的代码。 Tom:你能仔细检查我在这里写的内容吗?我不愿意非常明确地谈论这种事情...... @Andrew:构造函数实际上可能已经完成,并且赋值本身是可见的,但是在构造期间编写的其他字段可能对其他人不可见线程尚未。 看我的 cmets 来回答你的问题。我不相信你对为什么双重检查锁定被破坏的理解是完全正确的。【参考方案2】:

someObject 将在构建过程中的某个时刻变为非null。通常有两种情况:

    优化器已内联构造函数 构造函数未内联。

在第一种情况下,VM 会执行这段代码(伪代码):

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

所以在这种情况下,someObject 不是null 并且它指向的内存不是 100% 初始化的,即不是所有的构造函数代码都已运行!这就是double-checked locking 不起作用的原因。

在第二种情况下,来自构造函数的代码将运行,引用将被传回(就像在正常的方法调用中一样)并且 someObject 将被设置为引用的值 after所有初始化代码都已运行。

问题是没有办法告诉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。请注意,每一点都很重要;如果您省略双重分配,您将获得糟糕的性能或部分初始化的对象。如需完整说明,请购买该书。

【讨论】:

我不相信这是真正的内联问题,因为这样效果将在同一个线程中可见(我不相信它可以)。取而代之的是内存模型的变幻莫测,即首先将写入发布到其他线程。 特别是,我不相信执行构造的线程可以在构造函数期间将 someObject 视为非空,这与您的伪代码相反。如果您认为它可以看到非空值,请给出示例代码:) 正如我所说,在单线程代码中,内联通常不会导致任何问题。但是当你有多个线程时,没有启动构造函数的线程可以看到一个部分构造的对象。 我认为您只是在转移问题。在分配 mallocs 值之前,VM 中 someObject 的值是多少?此外,我怀疑优化器意识到 null 值将永远不会被使用,并将第一个示例优化为第二种形式,这会使 someObject 在一段时间内为 null。 我的论点是你不应该依赖它,即使它在单线程代码中工作。一旦你需要多线程,你就会看到非常奇怪的错误。因此,永远不要依赖编写取决于引用变为非空的时间的代码。【参考方案3】:

someObject 将是一个空指针,直到它从该类型的构造函数分配一个指针值。由于分配是从右到左的,因此可能让另一个线程在构造函数仍在运行时检查someObject。这将在分配指向变量的指针之前,因此someObject 仍然为空。

【讨论】:

【参考方案4】:

在另一个线程中,在构造函数完成执行之前,您的对象仍将显示为 null。这就是为什么如果构造被异常终止,引用将保持为空。

Object o = null;
try 
    o = new CtorTest();
 catch (Exception e) 
    assert(o == null); // i will be null

在哪里

class CtorTest 
    public CtorTest() 
        throw new RuntimeException("Ctor exception.");
    

确保在另一个对象上同步,而不是正在构造的对象。

【讨论】:

这很有趣; o 在构造函数运行时可以是 != null 但我猜如果你有一个 try-catch,优化器会将引用分配给一个临时变量。【参考方案5】:

对于您的第一个示例: someObject 在构造函数完成后变为非空。如果您要从另一个线程进行检查,则 someObject 在构造函数完成后将变为非空。请注意,您永远不应该从不同的线程访问未同步的对象,因此您的示例不应该在实际代码中以这种方式实现。

对于第二个示例, someObject 永远不会为 null,因为它是在 SomeClass 本身被构造并且 someObject 被创建并使用新创建的对象初始化之后构造的。线程也一样:不要在没有同步的情况下从不同的线程访问这个变量!

【讨论】:

【参考方案6】:

下面是一些测试代码,它显示在构造函数完成运行之前对象为空

public class Test 

  private static SlowlyConstructed slowlyConstructed = null;

  public static void main(String[] args) 
    Thread constructor = new Thread() 
      public void run() 
        Test.slowlyConstructed = new SlowlyConstructed();
      
    ;
    Thread checker = new Thread() 
      public void run() 
        for(int i = 0; i < 10; i++) 
          System.out.println(Test.slowlyConstructed);
          try  Thread.sleep(1000); 
          catch(Exception e) 
        
      
    ;

    checker.start();
    constructor.start();
  

  private static class SlowlyConstructed 
    public String s1 = "s1 is unset";
    public String s2 = "s2 is unset";

    public SlowlyConstructed() 
      System.out.println("Slow constructor has started");
      s1 = "s1 is set";
      try  Thread.sleep(5000); 
      catch (Exception e) 
      s2 = "s2 is set";
      System.out.println("Slow constructor has finished");
    

    public String toString() 
      return s1 + ", " + s2;
    
  

输出:

空值 缓慢的构造函数已启动 空值 空值 空值 空值 空值 慢构造器已完成 s1 已设置,s2 已设置 s1 已设置,s2 已设置 s1 已设置,s2 已设置 s1 已设置,s2 已设置

【讨论】:

你不能以这种方式进行证明。 这有什么问题?他问它什么时候发生,我写了一些代码来显示它什么时候发生。 由于线程的性质和不同 VM 的不同优化,您无法通过简单的代码示例证明或反驳该问题。

以上是关于java 对象在构造过程中何时变为非空?的主要内容,如果未能解决你的问题,请参考以下文章

将空值设置为列表中最接近的最后一个非空值 - LINQ

java 返回对象的非空属性的列表

Swift @escaping 仅适用于非空函数参数?

非空对象上的Android java.lang.NullPointerException [重复]

合并两个java bean对象非空属性(泛型)

java注解的方式对实体类进行非空校验