Guava的ImmutableList中的Javadoc说该类具有Guava的ImmutableCollection的属性,其中一个是线程安全:
线程安全。从多个线程同时访问此集合是安全的。
但是看看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;
}
...
}
这怎么是线程安全的?是什么保证了Builder
中执行的写操作与RegularImmutableList
读操作之间发生的关系?
根据Java memory model,只有五个案例(来自Javadoc为java.util.concurrent
)发生了之前的关系:
- 线程中的每个动作都发生在该线程中的每个动作之前,该动作在程序的顺序中稍后出现。
- 监视器的解锁(同步块或方法退出)发生在同一监视器的每个后续锁定(同步块或方法入口)之前。并且由于之前发生的关系是可传递的,因此在解锁之前线程的所有操作都会发生 - 在任何线程锁定该监视器之后的所有操作之前。
- 在每次后续读取同一字段之前,会发生对易失性字段的写入。易失性字段的写入和读取具有与进入和退出监视器类似的内存一致性效果,但不需要互斥锁定。
- 在启动线程中的任何操作之前发生对线程启动的调用。
- 线程中的所有操作都发生在任何其他线程从该线程上的连接成功返回之前。
这些似乎都不适用于此。如果某个线程构建列表并将其引用传递给其他一些线程而不使用锁(例如通过final
或volatile
字段),我看不出保证线程安全的原因。我错过了什么?
编辑:
是的,由于它是final
,对数组的引用的写入是线程安全的。所以这显然是线程安全的。我想知道的是各个元素的写作。阵列的元素既不是final
也不是volatile
。然而,它们似乎是由一个线程编写的,而另一个线程没有同步就读。
所以问题可以归结为“如果线程A写入final
字段,这是否保证其他线程不仅会看到写入而且还会看到所有A的先前写入?”
如果对象中的所有字段都是final
并且构造函数1没有泄漏this
,则JMM保证安全初始化(构造函数中初始化的所有值对读者都是可见的):
class RegularImmutableList<E> extends ImmutableList<E> {
final transient Object[] array;
^
RegularImmutableList(Object[] array) {
this.array = array;
}
}
The final field semantics保证读者会看到最新的数组:
在构造函数发布对新构造的对象的引用之后,所有初始化的效果必须在任何代码之前提交到内存。
感谢@JBNizet和@chrylis获取JLS的链接。
1 - “如果遵循这一点,那么当另一个线程看到该对象时,该线程将始终看到该对象的最终字段的正确构造版本。它还将看到那些最终字段引用的任何对象或数组的版本至少与最终字段一样最新。“ - JLS §17.5。
正如你所说:“线程中的每个动作都发生在该线程中的每个动作之前,该动作在程序的顺序中稍后出现。”
显然,如果一个线程可以在构造函数被调用之前以某种方式访问该对象,那么你就会被搞砸了。因此必须防止在构造函数返回之前访问对象。但是一旦构造函数返回,任何让另一个线程访问该对象的东西都是安全的,因为它发生在构造线程的程序顺序之后。
任何共享对象的基本线程安全性都是通过确保在构造函数返回之前不会发生允许线程访问对象的任何内容来实现的,从而确定构造函数可能在任何其他线程可能访问该对象之前发生的任何事情。
流程是:
调用构造函数的线程的程序顺序确保在完成所有2之后不会发生4的任何部分。
请注意,如果在构造函数返回后需要完成某些操作,则这同样适用,您可以在逻辑上将它们视为构造过程的一部分。同样地,部分工作可以由其他线程完成,只要需要看到另一个线程完成工作的任何东西都无法启动,直到与其他线程所做的工作建立了某些关系。
这不是100%回答你的问题吗?
重述:
这怎么是线程安全的?是什么保证了在Builder中执行的写操作和从RegularImmutableList读取之间发生的关系?
答案是在构造函数被调用之前阻止对象被访问的任何东西(必须是某些东西,否则我们将被完全搞砸)继续阻止对象被访问,直到构造函数返回之后。构造函数实际上是一个原子操作,因为没有其他线程可能在它运行时尝试访问该对象。一旦构造函数返回,无论调用构造函数的哪个线程允许其他线程访问对象,都必须在构造函数返回后发生,因为“线程中的[e] ach动作发生 - 在该线程中的每个动作之后发生按照程序的顺序。“
还有一次:
如果某个线程构建列表并将其引用传递给其他一些线程而不使用锁(例如通过final或volatile字段),我看不出保证线程安全的原因。我错过了什么?
线程首先构建列表,然后接下来传递它的引用。列表的构建“发生在 - 在该程序的顺序中稍后出现的那个线程中的每个动作之前”,因此在传递参考之前发生。因此,在完成列表构建之后,任何看到引用传递的线程都会发生。
如果不是这种情况,就没有好方法在一个线程中构造一个对象,然后让其他线程访问它。但这样做是完全安全的,因为无论使用何种方法将对象从一个线程交给另一个线程,都将建立必然的关系。
你在这里谈论两件不同的事情。
RegularImmutableList
及其array
是线程安全的,因为不会有任何并发写入和读取到该数组。只有并发读取。RegularImmutableList
无关,而是与其他线程如何看待它的引用。让我们说一个线程创建RegularImmutableList
并将其引用传递给另一个线程。对于另一个线程,看到引用已更新,现在指向新创建的RegularImmutableList
,您将需要使用synchronization
或volatile
。编辑:
我认为OP的关注点是JMM如何确保在从一个构建线程创建之后写入array
的任何内容在其引用传递给它们之后对其他线程可见。
这通过使用或volatile
或synchronization
发生。例如,当读者线程将RegularImmutableList
分配给volatile变量时,JMM将确保对数组的所有写入都闪存到主内存中,当其他线程从中读取时,JMM确保它将看到所有闪存写入。