昨天我了解到,多年来我一直错误地使用具有并发性的集合。
每当我创建一个需要被多个线程访问的集合时,我将其包装在其中一个Collections.synchronized *方法中。然后,每当变异集合时,我也将它包装在一个synchronized块中(我不知道为什么我这样做,我一定以为我在某处读过它。)
然而,在更仔细地阅读API之后,似乎在迭代集合时需要synchronized块。从API文档(对于Map):
当迭代任何集合视图时,用户必须手动同步返回的地图:
这是一个小例子:
List<O> list = Collections.synchronizedList(new ArrayList<O>());
...
synchronized(list) {
for(O o: list) { ... }
}
所以,鉴于此,我有两个问题:
为什么这甚至是必要的?我能想到的唯一解释是他们使用默认迭代器而不是托管线程安全迭代器,但是他们可以创建一个线程安全的迭代器并修复这个混乱,对吗?
更重要的是,这是完成了什么?通过将迭代放在同步块中,您可以防止多个线程同时进行迭代。但是另一个线程可以在迭代时改变列表,那么synchronized块如何帮助那里呢?不会在其他地方改变列表,不管它是否同步?我错过了什么?
感谢您的帮助!
答案 0 :(得分:6)
为什么这甚至是必要的?我能想到的唯一解释是 他们使用默认迭代器而不是托管线程安全 迭代器,但他们可以创建一个线程安全的迭代器并修复 这个烂摊子,对吧?
迭代一次只能处理一个元素。为了使Iterator
成为线程安全的,他们需要复制该集合。如果做不到这一点,对基础Collection
的任何更改都会影响您使用不可预测或未定义结果进行迭代的方式。
更重要的是,这是完成了什么?通过迭代 在同步块中,您正在阻止多个线程 同时迭代。但另一个线程可能会改变列表 迭代时,同步块如何帮助那里? 不会在其他地方改变列表与迭代一致 它是否同步?我错过了什么?
synchronizedList(List)
返回的对象的方法通过在实例上同步来工作。因此,当您在List
的{{1}}区块内时,没有其他线程可以在同一synchronized
内添加/删除。
答案 1 :(得分:4)
Collections.synchronizedList()
返回的对象的所有方法都与列表对象本身同步。每当从一个线程调用一个方法时,调用它的任何方法的每个其他线程都会被阻塞,直到第一个调用结束。
到目前为止一切顺利。
但是当你在next()
的<{strong>>到Iterator
之间时,这并不能阻止另一个线程修改集合。如果发生这种情况,您的代码将失败并显示ConcurrentModificationException
。但是如果你也在synchronized
块中进行迭代,并且你在同一个对象(即列表)上进行同步,这将阻止其他线程调用列表中的任何mutator方法,它们必须等到你的迭代线程释放列表对象的监视器。 关键是mutator方法对于与迭代器块相同的对象是synchronized
,这就是阻止它们。
请注意,尽管上述内容保证了基本的完整性,但并不能始终保证正确的行为。您可能在代码的其他部分中做出了在多线程环境中无法解决的假设:
List<Object> list = Collections.synchronizedList( ... );
...
if (!list.contains( "foo" )) {
// there's nothing stopping another thread from adding "foo" here itself, resulting in two copies existing in the list
list.add( "foo" );
}
...
synchronized( list ) { //this block guarantees that "foo" will only be added once
if (!list.contains( "foo" )) {
list.add( "foo" );
}
}
关于线程安全迭代器的问题,确实有一个列表实现,它叫做CopyOnWriteArrayList
。它非常有用,但正如API文档中所指出的那样,它仅限于少数几个用例,特别是当您的列表很少被修改但频繁迭代(以及如此多的线程)时,同步迭代会导致严肃的瓶颈。如果您使用不当,可能会大大降低应用程序的性能,因为列表的每次修改都会创建一个完整的新副本。
答案 2 :(得分:3)
在返回的列表上进行同步是必要的,因为内部操作在mutex
上同步,并且该互斥锁是this
,即同步集合本身。
这里有一些relevant code from Collections
,SynchronizedCollection
的构造函数,同步集合层次结构的根。
SynchronizedCollection(Collection<E> c) {
if (c==null)
throw new NullPointerException();
this.c = c;
mutex = this;
}
(还有另一个带有互斥锁的构造函数,用于初始化同步&#34;查看&#34;来自subList
等方法的集合。)
如果您在同步列表本身上进行同步,则 会阻止另一个线程在您对其进行迭代时改变列表。
同步集合本身同步的必要性存在,因为如果你同步其他任何东西,那么你想象的可能发生 - 另一个线程在你迭代它时改变集合,因为锁定的对象是不同。
答案 3 :(得分:1)
Sotirios Delimanolis answered你的第二个问题&#34;这是完成了什么?&#34;有效。我想扩大他对你第一个问题的回答:
为什么这甚至是必要的?我能想到的唯一解释是他们使用默认迭代器而不是托管线程安全迭代器,但是他们可以创建一个线程安全的迭代器并修复这个混乱,对吗?
有几种方法可以实现&#34;线程安全&#34;迭代器。与软件系统一样,存在多种可能性,它们在性能(活性)和一致性方面提供不同的权衡。在我的头顶,我看到了三种可能性。
<强> 1。锁定+失败
这是API文档建议的内容。如果在迭代它时锁定同步包装器对象(并且系统中的其余代码正确编写,以便突变方法调用也都通过同步包装器对象),则保证迭代看到内容的一致视图的集合。每个元素将被遍历一次。当然,缺点是其他线程在被迭代时无法修改甚至读取它。
这种变体将使用读写器锁来允许读取但不允许在迭代期间写入。但是,迭代本身可以改变集合,因此这会破坏读者的一致性。您必须编写自己的包装器才能执行此操作。
如果没有围绕迭代进行锁定并且其他人修改了集合,或者如果锁定并且某人违反了锁定策略,那么失败快速就会发挥作用。在这种情况下,如果迭代检测到集合已从其下变异,则会抛出ConcurrentModificationException
。
<强> 2。写入时复制强>
这是CopyOnWriteArrayList
等人采用的策略。这样的集合上的迭代器不需要锁定,它在迭代器期间总是显示一致的结果,并且它永远不会抛出ConcurrentModificationException
。但是,写入将始终复制整个阵列,这可能很昂贵。也许更重要的是,一致性的概念被改变了。在您迭代它时,集合的内容可能已经改变了 - 更准确地说,当您在过去中迭代其状态的快照时 - 所以您可能做出的任何决定现在可能已经过时了。
第3。弱一致
此策略由ConcurrentLinkedDeque
和类似集合使用。该规范包含weakly consistent的定义。这种方法也不需要任何锁定,迭代永远不会抛出ConcurrentModificationException
。但是一致性属性非常弱。例如,您可以尝试通过迭代ConcurrentLinkedDeque
并将遇到的每个元素添加到新创建的List
来复制{{1}}的内容。但是其他线程可能会在您重复迭代时修改deque。特别是,如果一个线程删除一个元素&#34;后面&#34;您已经迭代过的地方,然后添加一个元素&#34;提前&#34;在你迭代的地方,迭代可能会同时观察被移除的元素和添加的元素。因此,副本将有一个&#34;快照&#34;从来没有在任何时间存在过。雅得承认这是一个非常弱的一致性概念。
最重要的是,没有简单的概念可以使迭代器线程安全,从而解决这个问题&#34;。有几种不同的方式 - 可能比我在这里解释的更多 - 并且它们都涉及不同的权衡。任何一项政策都不太可能做出正确的事情&#34;在所有情况下,所有节目。