繁体   English   中英

什么时候java对象在构造期间变为非null?

[英]When does a java object become non-null during construction?

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

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

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

为了澄清一点,如果另一个线程要检查someObject为null而SomeClass()构造函数在完成的一半时,它是null还是非null?

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

SomeClass someObject = new SomeClass();

someObject不会是null?

如果另一个线程要在“构造期间”检查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 通常,有两种情况:

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

在第一种情况下,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。 请注意,每一位都很重要; 如果省略双重赋值,则会导致性能不佳或部分初始化对象。 如需完整说明,请购买本书。

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

从另一个线程开始,在构造函数完成执行之前,您的对象仍将显示为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.");
    }
}

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

这是一些测试代码,它显示对象为null,直到构造函数完成运行

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;
    }
  }
}

输出:

null
Slow constructor has started
null
null
null
null
null
Slow constructor has finished
s1 is set, s2 is set
s1 is set, s2 is set
s1 is set, s2 is set
s1 is set, s2 is set

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

对于第二个示例,someObject永远不会为null,因为它是在构造SomeClass本身之后构造的,并且someObject是使用新创建的对象创建和初始化的。 线程相同:不同步,不要从不同的线程访问此变量!

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM