Guava的ImmutableList的线程安全靠什么保证?

What guarantees the thread safety of Guava's ImmutableList?

Guava的JavadocImmutableList says that the class has the properties of Guava's ImmutableCollection,其中一个是线程安全:

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

但看看 ImmutableList 是如何由其构建器构建的 - Builder 将所有元素保存在 Object[] 中(这没关系,因为没有人说构建器是线程安全的) 并在构建时将该数组(或可能是副本)传递给 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);
        }
        ...
    }

}

RegularImmutableList 用这些元素做什么?如您所料,只需启动其内部数组,然后将其用于所有读取操作:

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

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

    ...
}

这是如何实现线程安全的?是什么保证了 happens-beforeBuilder 中执行的 writesreads 之间的关系] 来自 RegularImmutableList?

根据 Java memory model there is a happens-before relationship in only five cases (from the Javadocjava.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 似乎适用于此。如果某个线程构建列表并将其引用传递给其他一些线程而不使用锁(例如通过 finalvolatile 字段),我看不出是什么保证了线程安全。我错过了什么?

编辑:

是的,写入数组的引用是线程安全的,因为它是 final。所以这显然是线程安全的。 我想知道的是各个元素的写入。数组的元素既不是 final 也不是 volatile。然而,它们似乎由一个线程写入并由另一个线程读取而没有同步。

所以问题可以归结为"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?"

你在这里谈论的是两件不同的事情。

  1. 访问已构建的 RegularImmutableList 及其 array 是线程安全的,因为不会对该数组进行任何并发写入和读取。仅并发读取。

  2. 当您将它传递给另一个线程时,可能会发生线程问题。但这与 RegularImmutableList 无关,而与其他线程如何看待对它的引用有关。 假设一个线程创建 RegularImmutableList 并将其引用传递给另一个线程。要让另一个线程看到引用已更新并且现在指向新创建的 RegularImmutableList,您将需要使用 synchronizationvolatile.

编辑:

我认为 OP 担心的是 JMM 如何确保从一个构建线程创建后写入 array 的任何内容在其引用传递给其他线程后对其他线程可见。

这是通过使用或 volatilesynchronization 发生的。例如,当 reader 线程将 RegularImmutableList 分配给 volatile 变量时,JMM 将确保对数组的所有写入都闪存到主内存中,而当其他线程从中读取时,JMM 确保它将看到所有闪存写入.

如果对象中的所有字段都是final并且构造函数[=25中没有this的泄漏,JMM保证安全初始化(所有在构造函数中初始化的值对读者都是可见的) =]1:

class RegularImmutableList<E> extends ImmutableList<E> {

    final transient Object[] array;
      ^

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

The final field semantics 保证读者将看到最新的数组:

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


感谢@JBNizet 和@chrylis 对 JLS 的 link。

1 - “如果遵循这一点,那么当另一个线程看到该对象时,该线程将始终看到该对象最终字段的正确构造版本。它将还可以查看那些至少与最终字段一样最新的最终字段引用的任何对象或数组的版本。” - JLS §17.5.

如您所述:“线程中的每个操作都先于该线程中按程序顺序出现的每个操作之前。

显然,如果一个线程甚至可以在调用构造函数之前以某种方式访问​​该对象,那您就完蛋了。所以必须有一些东西阻止对象在其构造函数之前被访问 returns。但是一旦构造函数returns,任何让另一个线程访问该对象的东西都是安全的,因为它发生在构造线程的程序顺序之后。

任何共享对象的基本线程安全是通过确保在构造函数 returns 之前不会发生任何允许线程访问对象的事情来实现的,确定构造函数可能做的任何事情先于任何其他线程可能发生访问对象。

流量为:

  1. 对象不存在,无法访问。
  2. 一些线程调用对象的构造函数(或做任何其他需要让对象准备好被使用)。
  3. 该线程然后执行某些操作以允许其他线程访问该对象。
  4. 其他线程现在可以访问该对象。

调用构造函数的线程的程序顺序确保在完成 2 之前不会发生 4 的任何部分。

请注意,如果事情需要在构造函数之后完成,这同样适用 returns,您可以在逻辑上将它们视为构造过程的一部分。类似地,部分工作可以由其他线程完成,只要在与其他线程所做的工作建立某种关系之前,任何需要查看另一个线程完成的工作的东西都不能启动。

这不是 100% 回答了您的问题吗?

重述:

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

答案是在调用构造函数之前阻止对象被访问的任何东西(这必须是某种东西,否则我们会完全搞砸了)继续阻止对象被访问直到构造函数之后 returns。构造函数实际上是一个原子操作,因为当对象处于 运行 时,没有其他线程可能会尝试访问该对象。一旦构造函数returns,调用构造函数的线程为允许其他线程访问对象所做的任何事情都必须在构造函数returns之后发生,因为“[e]线程中的每个动作发生在之前该线程中的每个操作都按程序的顺序出现。"

还有,再来一次:

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?

线程首先构建列表,然后传递它的引用。列表 "happens-before every action in that thread that comes later in the program's order" 的构建因此发生在引用传递之前。因此,任何看到引用传递的线程都发生在列表构建完成之后。

否则,就没有好的方法在一个线程中构造一个对象,然后让其他线程访问它。但这样做是绝对安全的,因为无论您使用什么方法将对象从一个线程传递到另一个线程,都将建立必然的关系。