javajvmjava-memory-modeljls

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


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


Solution

  • 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.