Is it safe to assume that everything happened in a constructor is visible to threads running methods after object initialization?

108 views Asked by At

Lets have the following class:

public class MyClass {
  private final List<String> myList = new ArrayList<>(); //Not a thread-safe thing

  // Called on thread 1
  MyClass() {
    myList.add("foo");
  }

  // Called on thread 2
  void add(String data) {
    myList.add(data);
  }
}

Is it well-formed or not?

I was only able to find this:

An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.

https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.5

That means "thread 2" must see ArrayList, but may or may not see its content, right?

In other words, is it possible to have the following order:

T1: Creates MyClass along with ArrayList

T2: Accesses ArrayList

T1: Adds "foo" to the list

2

There are 2 answers

2
rzwitserloot On BEST ANSWER

Your thinking is incorrect. It's a sensible line of thought, but, this section of the JMM (JMM §17.5):

It will also see versions of any object or array referenced by those final fields that are at least as up-to-date as the final fields are.

Disagrees with the analysis. So, assuming you adhere to the rules set forth in that section of the JMM, which is:

Do not let the this ref escape from the constructor. e.g. if the constructor does someStaticField = this; or someOtherThing.hereYouGo(this);, any code that uses that this reference does not get the guarantee.

Then not only are the direct values safe (as in, for primitives, the value, and for objects, the reference (i.e. the pointer)), but so are that object's fields / that array's slots. "safe" here means: Any code that refers to it (other than via the above exception where you don't get this safety) cannot observe any of the final fields in a state as it was before the constructor finishes.

Hence, your snippet is well-formed.

Note of course that the act of assigning the value of a constructor call is, itself, not covered by any of this. Hence:

class Example {
  MyObject c;

  void codeInThread1() {
    c = new MyObject();
  }

  void codeInThread2() {
    System.out.println(c);
  }
}

May result in thread 2 printing null (because c is observed as null by thread 2), even if other effects the code has clearly indicates thread1 has progressed well past c = new MyObject();. The JMM does not guarantee that one thread's writes are observed by another thread unless a Happens-Before relationship is established.

However, the JVM does guarantee no sheared writes for object refs and a certain consistency: There are only 2 things that can be observed by thread 2. EITHER null, OR a fully initialized object, i.e., no state as it was before the constructor finished can be observed by thread 2.

0
user22309916 On

The "Hold on!" part of the accepted answer requires correction.

It's assumed there that the inner state of the object, stored in a final field, is guaranteed by the JMM to be visible in other threads at least as up-to-date as it was when the final field was assigned inside the constructor.

But actually, the JMM guarantees that the inner state will be as up-to-date as at the end of the constructor, where the final field was assigned.

The reason for the confusion is the following sentence in the JLS:

It will also see versions of any object or array referenced by those final fields that are at least as up-to-date as the final fields are.

The quote above can be interpreted both ways because it's a human-friendly text.
To find out precisely what is guaranteed we need to look at the actual rules (which the quote above describes in a human-friendly way): 17.5.1. Semantics of final Fields.
Let's apply these rules to our example (notice the names of the actions given in the comments).

public class MyClass {

  final List<String> myList = new ArrayList<>();

  MyClass() {
    myList.add("foo"); // w (internal write, e.g. myList.size=1)
    // f (freeze action which happens at the end of constructor)
  }

  void add(String data) {
    myList.add(data); // r1 (read of final field 'o.myList') and r2 (internal read, e.g. myList.size))
  }

  static MyClass o;

  public static void main(String[] args) throws InterruptedException {
    var thread1 = new Thread(() -> {
      o = new MyClass(); // a (publication of the object in thread1)
    });
    var thread2 = new Thread(() -> {
      o.add("test"); // r0 (read of the published object in thread2)
    });
    thread1.start();
    thread2.start();
    thread1.join();
    thread2.join();
  }
}

With this action names we can use the following rule from the JLS:

Given a write w, a freeze f, an action a (that is not a read of a final field), a read r1 of the final field frozen by f, and a read r2 such that hb(w, f), hb(f, a), mc(a, r1), and dereferences(r1, r2), then when determining which values can be seen by r2, we consider hb(w, r2). (This happens-before ordering does not transitively close with other happens-before orderings.)

As you can see, w (the write o.myList.size=1 inside MyClass() in thread1) happens-before* r2 (the read of o.myList.size in thread2).
As a result, the JMM guarantees that thread2 sees inner state of o.myList as it was at the end of the constructor in thread1.

You can apply the same logic to any other inner field (at any depth) of the o.myList object.

For more info about the JMM final guarantees I would recommend this presentation by Vladimir Sitnikov.


P.S. it's worth noting that freeze happens at the end of the constructor that writes into the final field:

Let o be an object, and c be a constructor for o in which a final field f is written. A freeze action on final field f of o takes place when c exits, either normally or abruptly.

Note that if one constructor invokes another constructor, and the invoked constructor sets a final field, the freeze for the final field takes place at the end of the invoked constructor.

this means in the following example thread2 may see myList as empty, because freeze happens at the end of private MyClass() {} constructor:

public class MyClass {

  final List<String> myList = new ArrayList<>();

  private MyClass() {}

  // in thread1
  MyClass(String str) {
    this();
    myList.add(str);
  }

  // in thread2
  void add(String data) {
    myList.add(data);
  }
}

Finally, those, who are curious about which of the object's constructors execute field initializers, should read 12.5. Creation of New Class Instances.