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
的读取之间的事前关系? / p>
根据Java memory model,仅在五种情况下存在事前发生的关系(来自java.util.concurrent
的{{3}}):
- 线程中的每个动作都会发生-在该线程中的每个动作之前,其顺序要比程序顺序晚。
- 在每个后续锁定(同步块或方法)之前,发生监视器的解锁(同步块或方法退出) 条目)。而且因为之前发生的关系 是可传递的,在解锁之前线程的所有操作 发生在任何线程锁定之后的所有动作之前 监视器。
- 对易失性字段的写操作发生在每次对该字段的后续读取之前。易失性字段的写入和读取具有相似的 内存一致性会像进入和退出监视器一样,但是 不需要互斥锁定。
- 在启动线程中的任何操作之前,发生了在线程上启动的调用。
- 线程中的所有动作发生-在任何其他线程从该线程上的联接成功返回之前。
这些似乎都没有在这里适用。如果某个线程构建列表并将其引用传递给其他一些线程而不使用锁(例如,通过final
或volatile
字段),我看不出有什么保证线程安全的。我想念什么?
编辑:
是的,由于对数组的引用为final
,因此将其写入数组是线程安全的。因此,这显然是线程安全的。
我想知道的是各个元素的文字。数组的元素既不是final
也不是volatile
。但是它们似乎是由一个线程编写的,而由另一个线程读取的却没有同步。
因此,问题可以归结为“如果线程A写入final
字段,是否可以确保其他线程不仅看到该写入,还可以看到A的所有先前写入?”
答案 0 :(得分:2)
如果对象中的所有字段均为final
并且没有从构造函数泄漏this
,则JMM保证安全的初始化(在构造函数中初始化的所有值对读者都是可见的) 1 < / sup>:
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。
答案 1 :(得分:1)
如您所说:“ 线程中的每个动作都会发生-在该线程中的每个动作之前,其执行顺序都在程序的顺序之后。” ”
很明显,如果线程甚至可以在调用构造函数之前以某种方式访问对象,那么您将被搞砸。因此,必须采取一些措施防止对象在其构造函数返回之前被访问。但是一旦构造函数返回,任何允许另一个线程访问该对象的事物都是安全的,因为它是在构造线程的程序顺序之后发生的。
通过确保在允许构造函数返回之前不会发生允许线程访问该对象的任何事情,可以确保任何共享对象的基本线程安全,从而确保构造函数可能执行的任何操作在任何其他线程可以访问该对象之前发生。
流为:
调用构造函数的线程的程序顺序可确保在完成所有2个操作之前,不会发生4个操作的一部分。
请注意,如果在构造函数返回后需要执行某些操作,则这同样适用,您可以在逻辑上将它们视为构建过程的一部分。同样,只要需要查看其他线程完成的工作的任何事情在与其他线程所做的工作建立某种关系之前就无法开始,则部分工作可以由其他线程完成。
那不是100%回答您的问题吗?
要重述:
这如何保证线程安全?什么能保证在Builder中执行的写入与从RegularImmutableList读取的事前发生关系?
答案是阻止对象在构造函数被调用之前被访问的任何方法(必须是某种东西,否则我们将完全被搞砸)继续阻止对象访问,直到构造函数返回。构造函数实际上是原子操作,因为在运行该对象时,没有其他线程可能尝试访问该对象。构造函数返回后,调用构造函数的线程允许其他线程访问该对象所做的任何事情都必须在构造函数返回之后发生,因为“线程中的每个动作都发生在该线程中的每个动作之后按照程序的顺序。”
还有一次:
如果某个线程在不使用锁的情况下(例如通过final或volatile字段)构建了列表并将其引用传递给其他一些线程,那么我看不到有什么保证线程安全的。我想念什么?
线程首先构建列表,然后再传递其引用。列表的构建“在该线程中按程序顺序出现的每个动作之前发生”,因此发生在引用传递之前。因此,任何看到引用传递的线程都会在列表构建完成之后发生。
如果不是这种情况,就没有好的方法可以在一个线程中构造一个对象,然后让其他线程访问该对象。但这是绝对安全的,因为无论使用哪种方法将对象从一个线程传递到另一个线程,都将建立必然的关系。
答案 2 :(得分:0)
您在这里谈论的是两件事。
对已建立的RegularImmutableList
及其array
的访问是线程安全的,因为不会对该数组进行任何并发写入和读取。仅并发读取。
当您将其传递给另一个线程时,可能会发生线程问题。但这与RegularImmutableList
无关,而与其他线程如何看待它有关。
假设一个线程创建了RegularImmutableList
并将其引用传递给另一个线程。要使另一个线程看到引用已更新,并且现在指向新创建的RegularImmutableList
,则需要使用synchronization
或volatile
。
编辑:
我认为OP关心的问题是JMM如何确保从一个构建线程创建的array
中写入的所有内容在其引用传递给其他线程后对其他线程可见。
这是通过使用或volatile
或synchronization
发生的。例如,当读取器线程将RegularImmutableList
分配给volatile变量时,JMM将确保对阵列的所有写入均被闪存到主存储器中,而当其他线程从其读取时,JMM将确保将看到所有闪存的写入。