简体   繁体   中英

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?"

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 constructor 1 :

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 .

As you stated: " Each action in a thread happens-before every action in that thread that comes later in the program's order. "

Obviously, if a thread could somehow access the object before the constructor was even invoked, you would be screwed. So something must prevent the object from being accessed before its constructor returns. But once the constructor returns, anything that lets another thread access the object is safe because it happens after in the constructing thread's program order.

Basic thread safety with any shared object is accomplished by ensuring that whatever allows threads to access the object does not take place until the constructor returns, establishing that anything the constructor might do happens before any other thread might access the object.

The flow is:

  1. The object does not exist and cannot be accessed.
  2. Some thread calls the object's constructor (or does whatever else is needed to get the object ready to be used).
  3. That thread then does something to allow other threads to access the object.
  4. Other threads can now access the object.

Program order of the thread invoking the constructor ensures that no part of 4 happens until all of 2 is done.

Note that this applies just the same if things need to be done after the constructor returns, you can just consider them logically part of the construction process. And similarly, parts of the job can be done by other threads so long as anything that needs to see work done by another thread cannot start until some relationship is established with the work that other thread did.

Does that not 100% answer your question?

To restate:

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

The answer is whatever prevented the object from being accessed before the constructor was even called (which has to be something, otherwise we'd be completely screwed) continues to prevent the object from being accessed until after the constructor returns. The constructor is effectively an atomic operation because no other thread could possibly attempt to access the object while it's running. Once the constructor returns, whatever the thread that called the constructor does to allow other threads to access the object necessarily takes place after the constructor returns because, "[e]ach action in a thread happens-before every action in that thread that comes later in the program's order."

And, one more time:

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?

The thread first builds the list and then next passes its reference. The building of the list "happens-before every action in that thread that comes later in the program's order" and thus happens-before the passing of the reference. Thus any thread that sees the passing of the reference happens-after the building of the list completed.

Were this not the case, there would be no good way to construct an object in one thread and then give other threads access to it. But this is perfectly safe to do because whatever method you use to hand the object from one thread to another will establish the necessarily relationship.

You are talking about two different things in here.

  1. Access to already built RegularImmutableList and its array is thread safe because there wont be any concurrent writes and reads to that array. Only concurrent reads.

  2. The threading issue can happen when you pass it to another thread. But that has nothing to do with RegularImmutableList but with how other threads see reference to it. Lets say one thread creates RegularImmutableList and passes its reference to another thread. For the other thread to see that the reference has been updated and is now pointing to new created RegularImmutableList you will need to use either synchronization or volatile .

EDIT:

I think the concern OP has is how JMM makes sure that whatever got written into the array after its creation from one building thread gets visible to other threads after its reference gets passed to them.

This happens by the use or volatile or synchronization . When for example reader thread assigns RegularImmutableList to volatile variable the JMM will make sure that all writes to array get flashed into main memory and when other thread reads from it JMM makes sure that it will see all flashed writes.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM