我昨天了解到,多年来我一直错误地使用具有并发性的集合。
每当我创建一个需要被多个线程访问的集合时,我将其包装在其中一个Collections.synchronized *方法中。然后,每当 变异 我也将它包装在一个同步块中(我不知道为什么我这样做,我一定以为我在某处读过它)。
但是,在更仔细地阅读API之后,似乎需要同步块 迭代 集合。从API文档(用于Map):
当迭代任何集合视图时,用户必须手动同步返回的地图:
这是一个小例子:
List<O> list = Collections.synchronizedList(new ArrayList<O>());
...
synchronized(list) {
for(O o: list) { ... }
}
所以,鉴于此,我有两个问题:
谢谢您的帮助!
为什么这甚至是必要的?我能想到的唯一解释是
他们使用默认迭代器而不是托管线程安全
迭代器,但他们可以创建一个线程安全的迭代器并修复
这个烂摊子吧?
迭代一次只能处理一个元素。为了 Iterator
为了线程安全,他们需要制作一个集合的副本。如果做不到,对底层的任何改变 Collection
会影响您如何使用不可预测或未定义的结果进行迭代。
更重要的是,这是什么实现的?通过迭代
在同步块中,您正在阻止多个线程
同时迭代。但另一个线程可能会改变列表
迭代时,同步块如何帮助那里?
不会在其他地方改变列表与迭代一致
它是否同步?我错过了什么?
返回的对象的方法 synchronizedList(List)
通过在实例上同步来工作。所以没有其他线程可以添加/删除相同的 List
而你在里面 synchronized
封锁了 List
。
基本情况
返回的对象的所有方法 Collections.synchronizedList()
被同步到列表对象本身。每当从一个线程调用一个方法时,每个调用它的任何方法的其他线程都会被阻塞,直到第一个调用完成。
到现在为止还挺好。
Iterare necesse est
但这并不能阻止另一个线程修改集合 在电话之间 至 next()
在它的 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文档所示,它仅限于少数几个用例,特别是当您的列表很少被修改但经常迭代(以及如此多的线程)时,同步迭代将导致严肃的瓶颈。如果您不恰当地使用它,它会极大地降低应用程序的性能,因为列表的每次修改都会创建一个完整的新副本。
必须在返回的列表上进行同步,因为内部操作在a上同步 mutex
那个互斥锁是 this
,即同步集合本身。
这是一些 相关代码来自 Collections
,建设者 SynchronizedCollection
,synchronized同步集合层次结构的根。
SynchronizedCollection(Collection<E> c) {
if (c==null)
throw new NullPointerException();
this.c = c;
mutex = this;
}
(还有另一个带有互斥锁的构造函数,用于初始化来自诸如的方法的同步“视图”集合 subList
。)
如果您在同步列表本身上进行同步,那么 不 防止另一个线程在迭代它时改变列表。
您同步同步集合本身的必要性存在,因为如果您同步其他任何内容,那么您可能会想到 - 当您迭代它时,另一个线程会改变集合,因为锁定的对象是不同的。
Sotirios Delimanolis 回答 你的第二个问题“这是什么成就?”有效。我想扩大他对你的第一个问题的答案:
为什么这甚至是必要的?我能想到的唯一解释是他们使用的是默认迭代器而不是托管线程安全迭代器,但是他们可以创建一个线程安全的迭代器并修复这个混乱,对吧?
有几种方法可以实现制作“线程安全”迭代器。与软件系统一样,存在多种可能性,它们在性能(活性)和一致性方面提供不同的权衡。在我的头顶,我看到三种可能性。
1.锁定+失败快
这是API文档的建议。如果在迭代它时锁定同步包装器对象(并且系统中的其余代码正确编写,以便突变方法调用也都通过同步包装器对象),则保证迭代看到内容的一致视图的集合。每个元素将被遍历一次。当然,缺点是其他线程在迭代时无法修改甚至读取集合。
这种变体将使用读写器锁来允许读取但不允许在迭代期间写入。但是,迭代本身可以改变集合,因此这会破坏读者的一致性。你必须编写自己的包装器才能做到这一点。
如果没有在迭代周围进行锁定并且其他人修改了集合,或者如果锁定并且某人违反了锁定策略,则失败快速发挥作用。在这种情况下,如果迭代检测到集合已从其下变异,则抛出 ConcurrentModificationException
。
2.写时复制
这是采用的策略 CopyOnWriteArrayList
等等。这样的集合上的迭代器不需要锁定,它在迭代器期间总是显示一致的结果,并且它永远不会抛出 ConcurrentModificationException
。但是,写入将始终复制整个阵列,这可能很昂贵。也许更重要的是,这个概念 一致性 被改变了。在迭代它时,集合的内容可能已经改变了 - 更准确地说,当你在其中迭代其状态的快照时 过去 - 所以你可能做出任何决定 现在 可能已经过时了。
3.弱一致
采用这种策略 ConcurrentLinkedDeque
和类似的集合。规范包含的定义 弱一致。这种方法也不需要任何锁定,迭代永远不会抛出 ConcurrentModificationException
。但是一致性属性非常弱。例如,您可能会尝试复制a的内容 ConcurrentLinkedDeque
通过迭代它并将遇到的每个元素添加到新创建的 List
。但是在你迭代时,其他线程可能正在修改deque。特别是,如果一个线程删除了你已经迭代的“后面”元素,然后在你正在迭代的地方“前面”添加一个元素,那么迭代可能会同时观察被移除的元素和添加的元素。因此,副本将具有在任何时间点从未实际存在的“快照”。雅得承认,这是一个非常弱的一致性概念。
最重要的是,没有一个简单的概念可以使迭代器线程安全,从而“修复这个混乱”。有几种不同的方式 - 可能比我在这里解释的更多 - 并且它们都涉及不同的权衡。任何一项政策都不可能在所有情况下为所有计划“做正确的事”。