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

发布于 2024-07-15 05:06:28 字数 467 浏览 8 评论 0 原文

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

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

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

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

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

SomeClass someObject = new SomeClass();

someObject 会为空吗?

Say you are creating a java object like so:

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

At what point does the someObject become non-null? Is it before the SomeClass() constructor runs or after?

To clarify a little, say if another thread was to check if someObject was null while the SomeClass() constructor was halfway through completion, would it be null or non-null?

Also, what would be the difference if someObject was created like so:

SomeClass someObject = new SomeClass();

Would someObject ever be null?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

迷路的信 2024-07-22 05:06:28

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

基本上,如果没有适当的锁定(或静态初始化器等给出的保证),请勿共享数据:) 说实话,内存模型非常棘手,就像一般的无锁编程一样。 尽量避免这种情况成为可能。

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

编辑:为了双重检查锁定的目的,如果您的字段是易失性并且如果您使用Java,则您可以摆脱这个 5或更高。 在 Java 5 之前,内存模型对此还不够强大。 不过,您需要完全正确地获得模式。 有关更多详细信息,请参阅《Effective Java》,第 2 版,第 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。 来自第 15.26.1 节

否则,需要三个步骤:

  • 首先,计算左侧操作数以生成一个变量。 如果本次评估完成
    突然,然后分配
    表达式突然完成
    同样的理由; 右边的操作数是
    未评估且未分配
    发生。
  • 否则,将计算右侧操作数。 如果这
    评估突然完成,然后
    赋值表达式完成
    突然出于同样的原因并且没有
    发生分配。

否则,右手的值
操作数转换为类型
左边的变量,受到
到值集转换(§5.1.13)到
适当的标准值设定
(不是扩展指数值集),
转换的结果是
存储到变量中。

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

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

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

If another thread were to check the someObject variable "during" construction, I believe it may (due to quirks in the memory model) see a partially initialized object. The new (as of Java 5) memory model means that any final fields should be set to their values before the object becomes visible to other threads (so long as the reference to the newly created object doesn't escape from the constructor in any other way) but beyond that there aren't many guarantees.

Basically, don't share data without appropriate locking (or guarantees given by static inializers etc) :) Seriously, memory models are seriously tricky, as is lock-free programming in general. Try to avoid this becoming a possibility.

In logical terms the assignment happens after the constructor runs - so if you observe the variable from the same thread it will be null during the constructor call. However, as I say there are memory model oddities.

EDIT: For the purposes of double-checked locking, you can get away with this if your field is volatile and if you're using Java 5 or higher. Prior to Java 5 the memory model wasn't strong enough for this. You need to get the pattern exactly right though. See Effective Java, 2nd edition, item 71 for more details.

EDIT: Here's my reasoning for arguing against Aaron's inlining being visible in a single thread. Suppose we have:

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

I believe this will always report true. From section 15.26.1:

Otherwise, three steps are required:

  • First, the left-hand operand is evaluated to produce a variable. If this evaluation completes
    abruptly, then the assignment
    expression completes abruptly for the
    same reason; the right-hand operand is
    not evaluated and no assignment
    occurs.
  • Otherwise, the right-hand operand is evaluated. If this
    evaluation completes abruptly, then
    the assignment expression completes
    abruptly for the same reason and no
    assignment occurs.

Otherwise, the value of the right-hand
operand is converted to the type of
the left-hand variable, is subjected
to value set conversion (§5.1.13) to
the appropriate standard value set
(not an extended-exponent value set),
and the result of the conversion is
stored into the variable.

Then from section 17.4.5:

Two actions can be ordered by a happens-before relationship. If one action happens-before another, then the first is visible to and ordered before the second.

If we have two actions x and y, we write hb(x, y) to indicate that x happens-before y.

  • If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).
  • There is a happens-before edge from the end of a constructor of an object to the start of a finalizer (§12.6) for that object.
  • If an action x synchronizes-with a following action y, then we also have hb(x, y).
  • If hb(x, y) and hb(y, z), then hb(x, z).

It should be noted that the presence of a happens-before relationship between two
actions does not necessarily imply that they have to take place in that order in an
implementation. If the reordering produces results consistent with a legal execution,
it is not illegal.

In other words, it's okay for weird stuff to happen even within a single thread but that mustn't be observable. In this case the difference would be observable, which is why I believe it would be illegal.

流心雨 2024-07-22 05:06:28

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

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

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

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

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

在第二种情况下,构造函数中的代码将运行,引用将被传回(就像在普通方法调用中一样),并且 someObject 将被设置为引用的值之后所有和每个初始化代码已运行。

问题是没有办法告诉java不要提前分配someObject。 例如,您可以尝试:

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

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

因此,这种行为是为了让优化器生成更快的代码,但在编写多线程代码时它可能会给你带来麻烦。 在单线程代码中,这通常不是问题,因为在构造函数完成之前不会执行任何代码。

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

PS:书“Effective Java,第二版" 包含适用于 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 will become non-null at some point during the construction. Typically, there are two cases:

  1. The optimizer has inlined the constructor
  2. The constructor is not inlined.

In the first case, the VM will execute this code (pseudocode):

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

So in this case, someObject is not null and it points to memory that is not 100% initialized, namely not all of the constructor code has been run! This is why double-checked locking doesn't work.

In the second case, the code from the constructor will run, a reference will be passed back (just like in a normal method call) and someObject will be set to the value of the refernce after all and every init code has run.

The problem is that there is no way to tell java not to assign someObject early. For example, you could try:

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

But since tmp is not used, the optimizer is allowed to ignore it, so it would produce the same code as above.

So this behavior is to allow the optimizer to produce faster code but it can bite you nastily when writing multi-threaded code. In single threaded code, this is usually not an issue since no code is executed until the constructor finishes.

[EDIT] Here is a good article which explains what's happening: http://www.ibm.com/developerworks/java/library/j-dcl.html

PS: The book "Effective Java, Second Edition" by Joshua Bloch contains a solution for Java 5 and 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;
}

Looks weird but should work on every Java VM. Note that every bit is important; if you omit the double assign, you'll either get bad performance or partially initialized objects. For a complete explanation, buy the book.

饭团 2024-07-22 05:06:28

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

someObject will be a null pointer right up until it is assigned a pointer value from the constructor of the type. Since assignment is from right to left it is possible for another thread to check someObject while the constructor is still running. This would be prior to the assignment of the pointer to the variable so someObject would still be null.

桃酥萝莉 2024-07-22 05:06:28

从另一个线程来看,您的对象仍然看起来为空,直到构造函数完成执行。 这就是为什么如果构造因异常而终止,引用将保持为空。

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

where

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

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

From another thread, your object will still look null until the constructor has finished executing. This is why if the construction is terminated by an exception, the reference will remain null.

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

where

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

Make sure to synchronize on another object, not the one being constructed.

绅刃 2024-07-22 05:06:28

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

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

For your first example: someObject becomes non-null AFTER the constructor has completed. If you would check from another thread, someObject would become non-null after the constructor has finished. Beware, you should never access unsynchronized objects from different threads, so your example should not be implemented that way in real-world code.

For the second example, someObject would never be null as it is constructed AFTER SomeClass itself is constructed and someObject is created&initialized with the newly created object. Same here for threads: don't access this variable from different threads without synchronisation!

把梦留给海 2024-07-22 05:06:28

下面是一些测试代码,显示在构造函数完成运行之前该对象为 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

Here's some test code which shows that the object is null until the constructor has finished running:

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

Output:

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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文