When does a java object become non-null during con

2019-03-14 22:42发布

问题:

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?

回答1:

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.



回答2:

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.



回答3:

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.



回答4:

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.



回答5:

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


回答6:

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!