代码之家  ›  专栏  ›  技术社区  ›  Lehane

在构造过程中,Java对象什么时候变成非空?

  •  16
  • Lehane  · 技术社区  · 15 年前

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

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

    SomeObject在什么时候变为非空?是在 SomeClass() 构造器运行还是在之后?

    为了澄清一点,比如说是否需要另一个线程来检查 someObject 体元() 构造函数已经完成了一半,它是空的还是非空的?

    另外,如果 某物 是这样创造的:

    SomeClass someObject = new SomeClass();
    

    某物 曾经是空的吗?

    6 回复  |  直到 6 年前
        1
  •  15
  •   Jon Skeet    15 年前

    如果另一个线程要检查 someObject 我相信在施工过程中会发生变化 可以 (由于内存模型中的特殊性)请参见部分初始化的对象。新的(如Java 5)内存模型意味着任何 最终的 在对象对其他线程可见之前(只要对新创建的对象的引用不会以任何其他方式从构造函数中逸出),应该将字段设置为它们的值,但除此之外,没有太多的保证。

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

    符合逻辑的 转让条款 之后 构造器运行-所以如果您观察变量 从同一条线 它在构造函数调用期间将为空。然而,正如我所说,有记忆模型的奇怪之处。

    编辑:为了进行双重检查锁定,您可以摆脱这种情况。 如果 你的领域是 volatile 如果 您使用的是Java 5或更高版本。在Java 5之前,内存模型不够强大。你得弄清楚模式 确切地 尽管如此。有关更多细节,请参见有效Java,第二版,项目71。

    编辑:这是我反对阿龙的观点的理由,因为它是一条线。假设我们有:

    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 :

    两个动作可以由一个发生在关系之前的动作来命令。如果一个动作先于另一个动作发生,那么第一个动作对第二个动作可见并在第二个动作之前排序。

    如果我们有两个动作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)。

    应该注意的是,A的存在发生在两人之间的关系之前 行动并不一定意味着他们必须按照这样的顺序进行 实施。如果重新排序产生的结果与合法执行一致, 这不是非法的。

    换句话说,即使是在一条线内,也可以发生奇怪的事情。 但不能被观察到 . 在这种情况下,差异 可见,这就是为什么我认为这是非法的。

        2
  •  8
  •   Aaron Digulla    15 年前

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

    1. 优化器已将构造函数内联
    2. 构造函数没有内联。

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

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

    所以在这种情况下, 某物 不是 无效的 它指向的内存不是100%初始化的,即不是所有的构造函数代码都已运行!这就是为什么 double-checked locking 不起作用。

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

    问题是没有办法告诉Java不要分配。 某物 早。例如,您可以尝试:

    SomeClass tmp = new SomeClass();
    someObject = tmp;
    

    但是由于没有使用tmp,优化器可以忽略它,因此它将生成与上面相同的代码。

    因此,这种行为允许优化器生成更快的代码,但在编写多线程代码时,它可能会让您感到恶心。在单线程代码中,这通常不是问题,因为在构造函数完成之前不会执行任何代码。

    [编辑]这是一篇很好的文章,解释了正在发生的事情: http://www.ibm.com/developerworks/java/library/j-dcl.html

    PS:书 Effective Java, Second Edition “Joshua Bloch包含Java 5和UP的解决方案:

    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虚拟机上工作。请注意,每一位都很重要;如果省略了双赋值,则会得到性能不佳的对象或部分初始化的对象。要得到完整的解释,请买这本书。

        3
  •  2
  •   Andrew Hare    15 年前

    someObject 将是一个空指针,直到从该类型的构造函数中为其分配一个指针值为止。因为作业是从右到左的 可能的 另一个线程检查 某物 当构造函数仍在运行时。这将在将指针赋给变量之前,因此 某物 仍然为空。

        4
  •  0
  •   omerkudat    15 年前

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

    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.");
        }
    }
    

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

        5
  •  0
  •   Kip    15 年前

    这是一些测试代码 在构造函数完成运行之前,对象为空。 :

    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
    
        6
  •  -1
  •   Kosi2801    15 年前

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

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