10

在我正在处理的多线程应用程序中,我们偶尔会ConcurrentModificationExceptions在列表中看到(主要是ArrayList,有时是向量)。但在其他时候,我认为正在发生并发修改,因为迭代集合似乎缺少项目,但没有抛出异常。我知道文档ConcurrentModificationException说您不能依赖它,但是我将如何确保我不会同时修改列表?并且将每个对集合的访问包装在同步块中是防止它的唯一方法吗?

更新:是的,我知道Collections.synchronizedCollection,但它不能防止有人在您迭代集合时修改集合。我认为至少我的一些问题正在发生,当有人在我迭代它时向集合中添加了一些东西。

第二次更新 如果有人想把提到的 synchronizedCollection 和像 Jason 那样的克隆与提到的 java.util.concurrent 和 jacekfoo 和 Javamann 这样的 apache 集合框架结合起来,我可以接受一个答案。

4

12 回答 12

6

根据您的更新频率,我最喜欢的一种是 CopyOnWriteArrayList 或 CopyOnWriteArraySet。他们在更新时创建一个新列表/集,以避免并发修改异常。

于 2008-09-16T18:05:56.417 回答
4

您最初的问题似乎是要求一个迭代器,它可以看到对底层集合的实时更新,同时保持线程安全。在一般情况下,这是一个非常昂贵的问题,这就是为什么标准集合类都没有这样做的原因。

有很多方法可以实现问题的部分解决方案,在您的应用程序中,其中一种可能就足够了。

Jason 给出了一种实现线程安全的特定方法,并避免抛出 ConcurrentModificationException,但只是以牺牲活性为代价。

Javamann 提到了包中的两个特定类java.util.concurrent,它们以无锁方式解决相同的问题,其中可伸缩性至关重要。这些仅随 Java 5 一起提供,但是已经有各种项目将包的功能反向移植到早期的 Java 版本中,包括这个,尽管它们在早期的 JRE 中不会有这么好的性能。

如果您已经在使用一些 Apache Commons 库,那么正如 jacekfoo指出的那样,apache 集合框架包含一些有用的类。

您还可以考虑查看Google 集合框架

于 2008-09-17T11:04:26.763 回答
3

查看java.util.concurrent以了解旨在更好地处理并发的标准 Collections 类的版本。

于 2008-09-16T17:51:57.260 回答
3

是的,您必须同步对集合对象的访问。

或者,您可以在任何现有对象周围使用同步包装器。请参阅Collections.synchronizedCollection()。例如:

List<String> safeList = Collections.synchronizedList( originalList );

但是所有代码都需要使用安全版本,即使在另一个线程修改时进行迭代也会导致问题。

要解决迭代问题,首先复制列表。例子:

for ( String el : safeList.clone() )
{ ... }

对于更优化、线程安全的集合,还可以查看java.util.concurrent

于 2008-09-16T17:52:40.787 回答
2

通常,如果您在迭代列表时尝试从列表中删除元素,则会收到 ConcurrentModificationException。

最简单的测试方法是:

List<Blah> list = new ArrayList<Blah>();
for (Blah blah : list) {
     list.remove(blah); // will throw the exception
}

我不确定你会如何解决它。您可能必须实现自己的线程安全列表,或者您可以创建原始列表的副本以进行写入,并拥有一个写入列表的同步类。

于 2008-09-16T17:54:17.677 回答
1

您可以尝试使用防御性复制,以便对其中的修改List不会影响其他人。

于 2008-09-16T17:52:09.397 回答
1

在同步块中包装对集合的访问是正确的方法。标准编程实践要求在处理跨多个线程共享的状态时使用某种锁定机制(信号量、互斥锁等)。

但是,根据您的用例,您通常可以进行一些优化以仅在某些情况下锁定。例如,如果您有一个经常读取但很少写入的集合,那么您可以允许并发读取,但在写入进行时强制执行锁定。仅当集合处于修改过程中时,并发读取才会导致冲突。

于 2008-09-16T17:55:49.840 回答
1

ConcurrentModificationException 是尽力而为,因为您要问的是一个难题。除了证明您的访问模式不会同时修改列表之外,没有什么好的方法可以在不牺牲性能的情况下可靠地做到这一点。

同步可能会阻止并发修改,这可能是您最终采用的方法,但最终可能会很昂贵。最好的办法可能是坐下来思考一下你的算法。如果你不能想出一个无锁的解决方案,那就求助于同步。

于 2008-09-16T17:56:21.573 回答
1

请参阅实施。它基本上存储一个int:

transient volatile int modCount;

并且当存在“结构修改”(如删除)时会增加。如果迭代器检测到 modCount 发生了变化,它会抛出并发修改异常。

同步(通过 Collections.synchronizedXXX)不会有好处,因为它不能保证迭代器的安全性,它只会通过 put、get、set ... 同步写入和读取。

请参阅 java.util.concurennt 和 apache 集合框架(当读取(非同步)多于写入时,它有一些经过优化的类在并发环境中可以正常工作 - 请参阅 FastHashMap。

于 2008-09-16T18:06:33.217 回答
1

您还可以在列表上的迭代上进行同步。

List<String> safeList = Collections.synchronizedList( originalList );

public void doSomething() {
   synchronized(safeList){
     for(String s : safeList){
           System.out.println(s);

     }
   }

}

这将在同步时锁定列表,并在您编辑或迭代列表时阻止所有尝试访问列表的线程。缺点是你会造成瓶颈。

这比 .clone() 方法节省了一些内存,并且可能会更快,具体取决于您在迭代中所做的事情......

于 2009-04-28T06:41:48.207 回答
-1

Collections.synchronizedList()将呈现一个名义上线程安全的列表,而 java.util.concurrent 具有更强大的功能。

于 2008-09-16T17:52:57.883 回答
-1

这将摆脱您的并发修改异常。但是,我不会谈论效率;)

List<Blah> list = fillMyList();
List<Blah> temp = new ArrayList<Blah>();
for (Blah blah : list) {
     //list.remove(blah);  would throw the exception
     temp.add(blah);
}
list.removeAll(temp);
于 2008-09-16T20:58:17.453 回答