Search code examples
javamultithreadingguavajava-memory-model

What guarantees the thread safety of Guava's ImmutableList?


The Javadoc in Guava's ImmutableList says that the class has the properties of Guava's ImmutableCollection, one of which is thread safety:

Thread safety. It is safe to access this collection concurrently from multiple threads.

But look at how the ImmutableList is built by its Builder - The Builder keeps all elements in a Object[] (that's okay since no one said that the builder was thread safe) and upon construction passes that array (or possibly a copy) to the constructor of RegularImmutableList:

public abstract class ImmutableList<E> extends ImmutableCollection<E>
implements List<E>, RandomAccess {
    ...
    static <E> ImmutableList<E> asImmutableList(Object[] elements, int length) {
      switch (length) {
        case 0:
          return of();
        case 1:
          return of((E) elements[0]);
        default:
          if (length < elements.length) {
            elements = Arrays.copyOf(elements, length);
          }
          return new RegularImmutableList<E>(elements);
      }
    }
    ...
    public static final class Builder<E> extends ImmutableCollection.Builder<E> {
        Object[] contents;
        ...
        public ImmutableList<E> build() { //Builder's build() method
          forceCopy = true;
          return asImmutableList(contents, size);
        }
        ...
    }

}

What does RegularImmutableList do with these elements? What you'd expect, simply initiates its internal array, which is then used for all read oprations:

class RegularImmutableList<E> extends ImmutableList<E> {
    final transient Object[] array;

    RegularImmutableList(Object[] array) {
      this.array = array;
    }

    ...
}

How is this be thread safe? What guarantees the happens-before relationship between the writes performed in the Builder and the reads from RegularImmutableList?

According to the Java memory model there is a happens-before relationship in only five cases (from the Javadoc for java.util.concurrent):

  • Each action in a thread happens-before every action in that thread that comes later in the program's order.
  • An unlock (synchronized block or method exit) of a monitor happens-before every subsequent lock (synchronized block or method entry) of that same monitor. And because the happens-before relation is transitive, all actions of a thread prior to unlocking happen-before all actions subsequent to any thread locking that monitor.
  • A write to a volatile field happens-before every subsequent read of that same field. Writes and reads of volatile fields have similar memory consistency effects as entering and exiting monitors, but do not entail mutual exclusion locking.
  • A call to start on a thread happens-before any action in the started thread.
  • All actions in a thread happen-before any other thread successfully returns from a join on that thread.

None of these seem to apply here. If some thread builds the list and passes its reference to some other threads without using locks (for example via a final or volatile field), I don't see what guarantees thread-safety. What am I missing?

Edit:

Yes, the write of the reference to the array is thread-safe on account of it being final. So that's clearly thread safe. What I was wondering about were the writes of the individual elements. The elements of the array are neither final nor volatile. Yet they seem to be written by one thread and read by another without synchronization.

So the question can be boiled down to "if thread A writes to a final field, does that guarantee that other threads will see not just that write but all of A's previous writes as well?"


Solution

  • JMM guarantees safe initialization (all values initialized in the constructor will be visible to readers) if all fields in the object are final and there is no leakage of this from constructor1:

    class RegularImmutableList<E> extends ImmutableList<E> {
    
        final transient Object[] array;
          ^
    
        RegularImmutableList(Object[] array) {
            this.array = array;
        }
    }
    

    The final field semantics guarantees that readers will see an up-to-date array:

    The effects of all initializations must be committed to memory before any code after constructor publishes the reference to the newly constructed object.


    Thank you to @JBNizet and to @chrylis for the link to the JLS.

    1 - "If this is followed, then when the object is seen by another thread, that thread will always see the correctly constructed version of that object's final fields. 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." - JLS §17.5.