如何在多线程代码中避免ConcurrentModificationException异常

8
每当我们使用java.util集合类时,如果一个线程在另一个线程正在使用迭代器遍历它时更改了集合,则对iterator.hasNext()iterator.next()的任何调用都会抛出ConcurrentModificationException异常。即使是同步的集合包装类SynchronizedMapSynchronizedList也只是有条件的线程安全,这意味着所有单个操作都是线程安全的,但是复合操作(其中控制流取决于先前操作的结果)可能会受到线程问题的影响。问题是:如何在不影响性能的情况下避免此问题。注意:我知道CopyOnWriteArrayList

2
那么你已经知道了解决方案。你不能两全其美:并发访问需要一个并发结构。 - Marko Topolnik
1
这非常具体于情况,并且取决于您的使用方式。此外,需要考虑为什么不能使集合访问和修改同步。 - Amit Deshpande
3
你的具体问题是什么?你所谈论的是一些通用性的问题。 - Adam Arold
我会返回一个集合的副本,以便于“迭代”,就像“CopyOnWriteArrayList”所做的那样。 - Amit Deshpande
我经常(但不总是)使用CopyOnWriteArrayList来解决这些类型的问题。然而,有时它可能非常昂贵。对于这个问题,我只想听听一些关于这个问题的意见和替代方法。我将接受我认为更好(通用)的替代方案作为答案。 - Bruno Simões
与https://dev59.com/jHI-5IYBdhLWcg3w0cKG相关。 - Raedwald
4个回答

2
您可以像上面提到的那样使用CopyOnWriteArrayListConcurrentHashMap,或者您可以使用与CAS一起工作的Atomic*类。如果您不知道Atomic*类,它们绝对值得一看!您可以查看问题。
因此,要回答您的问题,您必须为任务选择正确的工具。由于您没有与我们分享上下文,我只能猜测。在某些情况下,CAS会表现得更好,而在其他情况下,则是并发集合。
如果有什么不清楚的地方,您可以随时查看官方Oracle Trails:Lesson: Concurrency

不确定ConcurrentHashMap如何帮助他,因为锁定的是映射的各个段。 - Yair Zaslavsky
1
他提到的synchronized装饰器类会锁定整个Map。而另一方面,ConcurrentHashMap只会在你修改/删除时锁定一个桶。 - Adam Arold
@Adam Arold - 我明白,但我认为这可能太冒险了。如果我没记错的话,同步装饰器并没有使用 RW-Lock,而是使用“synchronized”,这就是为什么我建议在我的评论中使用 RWLock 进行锁定。 - Yair Zaslavsky
我明白了。嗯,很难想出一个单一的情况,这些装饰器是好的。 - Adam Arold

1
这是因为“标准”Java集合不是线程安全的,因为它们没有同步。当使用多个线程访问您的集合时,您应该查看java.util.concurrent包。
在Java 5之前没有这个包,人们必须执行手动同步:
synchronized(list) {
   Iterator i = list.iterator(); // Must be in synchronized block
   while (i.hasNext())
       foo(i.next());
}

或者使用

Collections.synchronizedList(arrayList);

但是两者都无法提供完整的线程安全功能。

通过使用这个包,对集合的所有访问都是原子性的,并且一些类提供了在构造迭代器时列表状态的快照(参见CopyOnWriteArrayList)。CopyOnWriteArrayList 在读取时很快,但如果您执行多次写入,则可能会影响性能。

因此,如果不需要 CopyOnWriteArrayList,可以看看 ConcurrentLinkedQueue,它提供了一个“弱一致性”迭代器,永远不会抛出 ConcurrentModificationException,并保证遍历元素时按照迭代器构造时的顺序进行。除非您需要更频繁地访问特定索引处的元素而不是遍历整个集合,否则这个方法在所有方面都很高效。

另一个选项是使用ConcurrentSkipListSet,它提供了期望的平均log(n)时间成本,用于包含、添加和删除操作及其变体。插入、删除和访问操作可以安全地由多个线程并发执行,而且迭代器是弱一致性的

哪些并发(线程安全)集合取决于您最常执行的操作类型。由于它们都是Java Collection框架的一部分,因此您可以根据需要随时进行交换。


评论太笼统了。你应该指明哪些类可以帮助他。如果你不知道如何使用它,java.util.concurrent对并发性并不是一个神奇的解决方案。 - Yair Zaslavsky
OP已经知道不仅是包,而且还有解决他问题的特定类。@AmitD即使作为评论也不会很好 :) 为了记录,Yanick,我不是你的downvoters之一。 - Marko Topolnik
抱歉,我没有注意到CopyOnWriteArrayList的提示。答案已经修正。 - Yanick Rochon

1

我认为你提出了一个有趣的问题。
我尝试思考一下,例如像其他人建议的ConcurrentHashMap是否可以帮助解决问题,但我不确定因为锁是基于段的。
在这种情况下,我会锁定对您的集合的访问,使用ReaderWriterLock
我选择这个锁的原因是因为我觉得这需要锁定(就像你所解释的——迭代由多个操作组成),
而且因为在读取线程的情况下,如果没有写入线程在处理集合,则不希望它们等待锁定。
感谢@Adam Arold,我注意到你建议的“同步装饰器”——但是我觉得这个装饰器对你的需求来说“太强大”了,因为它使用了同步,并且不会区分N个读者和M个写作者的情况。


1
请阅读我有关 ConcurrentHashMap 的问题:ConcurrentHashMap 内部是如何工作的? - Adam Arold
1
@Adam Arold - 感谢你指出这个问题。我学到了新东西。唯一的问题(就我所看到的)是你把他限制在了 Map(或者说 ConcurrentMap)接口上。如果可能的话,我会尝试找到一个更通用的解决方案,但你在其他评论中是正确的——这确实取决于你的需求。 - Yair Zaslavsky
1
对于通用解决方案,您可以查看 Atomic* 类。 - Adam Arold

0

如果您有一个未封装的非线程安全类的Collection对象,那么无法防止Collection的误用,因此可能会出现ConcurrentModificationException

其他答案建议使用线程安全的Collection类,例如由java.util.concurrent提供的类。但是,您应该考虑封装Collection对象:将对象设置为private,并且使您的类提供更高级别的抽象(例如addPersonremovePerson),代表调用者操作Collection,并且没有任何getter方法返回对Collection的引用。然后,可以相当容易地对封装数据执行不变量(例如“每个人都有一个非空名称”)进行强制执行,并使用synchronized提供线程安全性。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接