17

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

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

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

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

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

SomeClass someObject = new SomeClass();

someObject永远是空的吗?

4

6 回答 6

16

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

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

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

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

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

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

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

于 2009-03-25T14:44:11.870 回答
9

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

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

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

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

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

在第二种情况下,来自构造函数的代码将运行,引用将被传回(就像在正常的方法调用中一样),并且在所有初始化代码都运行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。请注意,每一点都很重要;如果您省略双重分配,您将获得糟糕的性能或部分初始化的对象。要获得完整的解释,请购买这本书。

于 2009-03-25T14:46:02.817 回答
2

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

于 2009-03-25T14:44:47.920 回答
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.");
    }
}

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

于 2009-03-25T15:01:15.743 回答
-1

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

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

于 2009-03-25T14:45:32.867 回答
-1

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

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 已设置
于 2009-03-25T15:01:43.077 回答