Java线程安全列表
我有一个List,它可以在线程安全上下文中使用,也可以在非线程安全上下文中使用。 它将是哪一个,是不可能事先确定的。
在这种特殊情况下,每当列表进入非线程安全上下文时,我都会使用它进行包装
Collections.synchronizedList(…)
但我不想包装它,如果不进入非线程安全的上下文。 Fe,因为列表很大并且使用密集。
我读过Java,它的优化策略是严格的multithreading – 如果你没有正确地同步你的代码,不能保证在线程间上下文中正确执行 – 它可以显着重新组织代码,提供一致性仅在一个线程的上下文中(参见http://java.sun.com/docs/books/jls/third_edition/html/memory.html#17.3 )。 铁,
OP1; OP2; OP3;
可能会重组
OP3; OP2; OP1;
,如果它产生相同的结果(在单线程上下文中)。
现在我想知道,如果我
-
填写我的列表,然后再用synchronizedList包装它,
-
把它包起来,
-
然后由不同的线程使用
, – 是否有可能,不同的线程会看到此列表仅部分填充或根本未填充? JVM可能会推迟(1)直到(3)之后? 是否有一种正确而快速的方法使(大)List成为线程安全的非线程安全的?
当您通过线程安全的方式将列表提供给另一个线程时(例如使用synchronized块,volatile变量或AtomicReference
),可以保证第二个线程看到整个列表处于传输时的状态(或任何以后的状态,但不是早期的状态)。
如果之后不进行更改,则也不需要synchronizedList。
编辑(在评论之后,备份我的声明):
我假设如下:
-
我们有一个易变的变量
list
。volatile List
list = null; -
线程A:
- 创建一个List L并用元素填充L.
- 将
list
设置为指向L(这意味着将L写入list
) - 对L.没有进一步的修改
样本来源:
public void threadA() { List
L = new ArrayList (); L.add("Hello"); L.add("World"); list = l; } -
线程B:
- 从
list
读取K. - 迭代K,打印元素。
样本来源:
public void threadB() { List
K = list; for(String s : K) { System.out.println(s); } } - 从
-
所有其他线程都不会触及列表。
现在我们有了这个:
- 线程A中的操作1-A和2-A按程序顺序排序,因此1在2之前。
- 线程B中的动作1-B和2-B按程序顺序排序,因此1在2之前。
- 线程A中的动作2-A和线程中的动作1-B按同步顺序排序 ,因此2-A出现在1-B之前,因为
对volatile变量(第8.3.1.4节)的写入v与任何线程对v的所有后续读取同步(其中后续根据同步顺序定义)。
-
before-before -order是各个线程的程序顺序和同步顺序的传递闭包。 所以我们有:
1-A发生在2-A发生之前 – 在1-B发生之前 – 在2-B之前发生
因此1-A发生在2-B之前。
- 最后,
如果一个动作发生在另一个动作之前,那么第一个动作在第二个动作之前是可见的并且在第
所以我们的迭代线程真的可以看到整个列表,而不仅仅是它的某些部分。 因此,使用单个volatile变量传输列表就足够了,在这种简单的情况下我们不需要同步。
关于线程A的程序顺序,还有一个编辑(这里,因为我有比注释中更多的格式自由)。(我还在上面添加了一些示例代码。)
从JLS(部分程序顺序 ):
在由每个线程t执行的所有线程间动作中,t的程序顺序是反映根据t的线程内语义将执行这些动作的顺序的总顺序。
那么,线程A的线程内语义是什么?
以上几段 :
内存模型确定可以在程序的每个点读取哪些值。 隔离中每个线程的操作必须表现为受该线程语义的控制,但每次读取所看到的值都由内存模型决定。 当我们参考这个时,我们说该程序遵循线程内语义 。 线程内语义是单线程程序的语义,允许基于线程内读取操作所看到的值完全预测线程的行为。 为了确定执行中的线程t的动作是否合法,我们只是评估线程t的实现,因为它将在单线程上下文中执行,如本规范其余部分所定义。
本规范的其余部分包括第14.2节(块) :
通过从第一个到最后一个(从左到右)按顺序执行每个局部变量声明语句和其他语句来执行块。
因此, 程序顺序确实是程序源代码中给出语句/表达式的顺序。
因此,在我们的示例源中,内存操作创建一个新的ArrayList , 添加“Hello” , 添加“World” ,并分配到list
(前三个由更多的子动作组成)确实是按此程序顺序 。
(VM不必按此顺序执行操作,但此程序顺序仍然有助于发生之前的顺序,从而有助于其他线程的可见性。)
如果你填写你的列表,然后将它包装在同一个线程中,你将是安全的。
但是要记住以下几点:
-
Collections.synchronizedList()
只保证低级线程安全。 复杂的操作,如if ( !list.contains( elem ) ) list.add( elem );
仍然需要自定义同步代码。 - 如果任何线程可以获得对原始列表的引用,则即使此保证也是无效的。 确保不会发生这种情况。
- 首先获得正确的function,然后您可以开始担心同步太慢。 我很少遇到Java同步速度严重的代码。
更新:我想在JLS中添加一些摘录,希望能够澄清一些问题。
如果x和y是同一个线程的动作,并且x在程序顺序中出现在y之前,那么hb(x,y)。
这就是为什么填充列表然后将其包装在同一个线程中是一个安全的选择。 但更重要的是:
这对程序员来说是一个非常有力的保证。 程序员不需要推理重新排序以确定他们的代码包含数据竞争。 因此,在确定其代码是否正确同步时,他们不需要推理重新排序。 一旦确定代码被正确同步,程序员就不必担心重新排序会影响他或她的代码。
消息很明确:确保您按照编写代码的顺序执行的程序不包含数据争用,并且不用担心重新排序。
如果遍历比写入更频繁,我会查看CopyOnWriteArrayList 。
ArrayList的线程安全变体,其中所有可变操作(添加,设置等)都是通过创建底层数组的新副本来实现的。
看看AtomicInteger (和类似的)如何实现线程安全和不同步。 该机制不引入同步,但如果需要同步,它会优雅地处理它。