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

时间:2009-03-25 14:39:57

标签: java constructor null order-of-execution

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

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

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

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

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

SomeClass someObject = new SomeClass();

someObject是否会为空?

6 个答案:

答案 0 :(得分:16)

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

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

逻辑术语中,赋值在构造函数运行后发生 - 所以如果从同一个线程观察变量,它将在null期间为null构造函数调用。但是,正如我所说,存在奇怪的记忆模型。

编辑:出于双重检查锁定的目的,如果您的字段为volatile ,您可以使用此 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.113节)转换为   适当的标准值集   (不是扩展指数值集),   转换的结果是   存储在变量中。

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

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

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

答案 1 :(得分:8)

someObject在施工期间的某个时刻将变为非null。通常,有两种情况:

  1. 优化器已内联构造函数
  2. 构造函数未内联。
  3. 在第一种情况下,VM将执行此代码(伪代码):

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

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

    在第二种情况下,构造函数中的代码将运行,引用将被传回(就像在普通方法调用中一样)并且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。请注意,每一位都很重要;如果省略双重赋值,则会导致性能不佳或部分初始化对象。如需完整说明,请购买本书。

答案 2 :(得分:2)

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

答案 3 :(得分:0)

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

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

答案 4 :(得分:0)

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

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

答案 5 :(得分:-1)

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

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