关于Java:检测并发修改?

关于Java:检测并发修改?

Detecting concurrent modifications?

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

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

第二次更新如果有人想像Jason一样提到提到的synchronizedCollection和克隆,再加上像jacekfoo和Javamann这样提到的apache collections框架,我可以接受一个答案。


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


您最初的问题似乎是要求一个迭代器,该迭代器在保持线程安全的同时查看对基础集合的实时更新。在一般情况下,这是一个难以解决的昂贵问题,这就是为什么标准集合类都不做的原因。

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

Jason提供了一种特定的方法来实现线程安全,并避免引发ConcurrentModificationException,但这只是以牺牲活动性为代价。

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

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

您也可以考虑查看Google收藏夹框架。


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

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

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

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

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

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

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


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


通常,如果要在迭代过程中尝试从列表中删除一个元素,则会收到ConcurrentModificationException。

最简单的测试方法是:

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

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


您还可以同步列表中的Iteratin。

1
2
3
4
5
6
7
8
9
10
11
List<String> safeList = Collections.synchronizedList( originalList );

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

     }
   }

}

这将锁定列表,使其处于同步状态,并阻止所有在尝试编辑或迭代列表时尝试访问该列表的线程。缺点是您会创建瓶颈。

这样可以通过.clone()方法节省一些内存,并且可能会更快,具体取决于您在迭代中正在执行的操作...


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

1
transient volatile int modCount;

并且在存在"结构修改"(例如remove)时增加。如果迭代器检测到modCount已更改,则它将引发并发修改异常。

同步(通过Collections.synchronizedXXX)不会做得很好,因为它不能保证迭代器的安全性,它只能通过put,get,set来同步读写操作。

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


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

同步可能会阻止并发修改,这最终可能是您要采取的措施,但最终可能会付出高昂的代价。最好的办法可能是坐下来思考一下您的算法。如果您无法提出无锁解决方案,则请使用同步。


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

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


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


这将消除您的并发修改异常。但是我不会讲效率;)

1
2
3
4
5
6
7
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);


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


推荐阅读