Java 这是潜在的ConcurrentModificationException吗?我的解决方案正确吗?

Java 这是潜在的ConcurrentModificationException吗?我的解决方案正确吗?,java,multithreading,Java,Multithreading,我在一个项目中,有一个类如下 class Container{ List<Container> dependencies; //... other members and methods. addDependent(Container con){ for(Container c : dependencies){ if(c.id == con.id) return; dependencies.add(c); } } } 类容

我在一个项目中,有一个类如下

class Container{
  List<Container> dependencies;
  //... other members and methods.
  addDependent(Container con){
    for(Container c : dependencies){
      if(c.id == con.id) return;
      dependencies.add(c);
    }
  }
}  
类容器{
列出依赖关系;
//……其他成员和方法。
addDependent(容器con){
for(容器c:依赖项){
if(c.id==con.id)返回;
添加(c);
}
}
}  
我们计算一个容器列表,如下所示。请注意,列表是巨大的,每次迭代的计算都是复杂的(我在这里只保留相关性)

List=//已初始化;
用于(容器c:列表){
c、 依赖性(c);
//用于填充容器的其他属性的其他计算。
}
此代码段位于单个线程中,工作正常,只是速度很慢,只有列表大小的bcoz。现在,我尝试将列表分成四个子列表,在四个线程中运行相同的代码,并在完成后将它们重新组合在一起(使用了同步器
CountdownLatch

现在我得到了
ConcurrentModificationException
错误。在做了一些梳理之后,我使用一个ThreadLocal变量在第一个代码片段中包装“列表依赖项”,CME就消失了


然而,问题是,我不太清楚为什么首先会发生异常,我的解决方案是否正确,或者在将来某个时候它会再次爆发

修改正在迭代的任何集合(除非通过迭代器进行修改)将导致出现
ConcurrentModificationException
。然而,在看了你们的课程之后,我想提出一个替代方案

现在,您正在将所有对象存储在
列表中
,并测试每个对象是否为成员。如果将此结构切换到一个
集合
,该集合只强制每种类型的对象中的一种,可能会使代码更清晰

class Container{
    Set<Container> dependencies = new HashSet<>();

    public void addDependent(Container con) {
       dependencies.add(c);
    }

    public boolean equals(Container c) {
       return c.id = this.id;
    }

    public int hashCode() {
       // etc
    }
}
类容器{
Set dependencies=newhashset();
公共void addDependent(容器con){
添加(c);
}
公共布尔等于(容器c){
返回c.id=this.id;
}
公共int hashCode(){
//等
}
}
请注意,您必须在
容器
对象上实现
equals()
hashCode()
,以便
能够区分
容器

关于并发修改异常 此异常不一定在多线程代码中引发。当您在迭代集合时修改集合时会发生这种情况。即使在单线程应用程序中也会出现此异常。例如,在For-each循环中,如果删除或向列表中添加元素,则最终会得到一个
ConcurrentModificationException

因此,向代码中添加同步并不一定能解决问题。一些替代方法包括制作要迭代的数据的副本,或者使用接受修改的迭代器(即ListIterator),或者使用带有快照迭代器的集合

显然,在多线程代码中,您仍然需要注意同步以避免进一步的问题

在上面的代码中,您正好遇到了这个问题。你写道:


for(容器c:依赖项){
if(c.id==con.id)返回;
添加(c);
}

这可能会修改for each循环中迭代下的相同集合,并且每次不满足if语句中计算的条件时,都可能会得到一个CME

让我举几个例子:

假设您希望在迭代集合时从集合中删除项。要避免出现
ConcurrentModificationException
异常,您可以选择以下选项:

List<Book> books = new ArrayList<Book>();
books.add(new Book(new ISBN("0-201-63361-2")));
books.add(new Book(new ISBN("0-201-63361-3")));
books.add(new Book(new ISBN("0-201-63361-4")));

在多线程环境中,您可能会考虑在迭代之前创建集合的副本,这样,允许其他人修改原始集合而不影响迭代:

synchronized(this.books) {
   List<Book> copyOfBooks = new ArrayList<Book>(this.books)
}
for(Book book : copyOfBooks) {
   System.out.println(book);
}
synchronized(this.books){
List copyOfBooks=new ArrayList(this.books)
}
用于(书籍:书籍副本){
系统输出打印(图书);
}

可选地,您可以考虑使用快照迭代器使用其他类型的集合,如 java .UTI.CONCURNTRONCONTRONEAREAR RayList,它保证不抛出<代码> CONTRONTRONGUSTICIONEXPRECTOR> <代码>。但请先阅读文档,因为这种类型的集合并不适用于所有场景

我同意托德的观点,移动到一个集合会更有效率,特别是因为海报表明依赖性越来越大。然而,除非您能够得到一组分布良好的hashCode值,否则使用TreeSet可能更有效。如果您这样做,您将需要正确实现可比较的接口(请仔细阅读)

尽管我们已经离开了最初的实现,但我想指出的是

addDependent(Container con){
  for(Container c : dependencies){
    if(c.id == con.id) return;
    dependencies.add(c);
  }
}
将c添加到每个不匹配项的依赖项中。例如,如果依赖项中容器中的现有id为(1、2、3、4、5),并且您使用id为6的容器调用addDependent,那么最终将得到(1、2、3、4、5、6、6、6、6、6、6)。此外,有了这个bug,您永远不会向依赖项添加任何内容,因为您只会在列表已经非空的情况下添加。您应该已将add移动到for循环之外:

addDependent(Container con){
  for(Container c : dependencies){
    if(c.id == con.id) return;
  }
  dependencies.add(c);
}

这本可以正确工作并消除CME,但对于大型依赖项列表,集合方法的性能会更好。

是否有理由使用
列表而不是
集合
?您正在修改正在迭代的相同集合。这导致了CME,甚至与多线程无关。如果在同步环境中使用此列表,则不能使用
ArrayList
。必须执行以下操作之一:1)使用
集合.synchronizedList()
方法,或2)使用其线程安全变量:
CopyOnWriteArrayList
。作为@EdwinDalorzo
synchronized(this.books) {
   List<Book> copyOfBooks = new ArrayList<Book>(this.books)
}
for(Book book : copyOfBooks) {
   System.out.println(book);
}
addDependent(Container con){
  for(Container c : dependencies){
    if(c.id == con.id) return;
    dependencies.add(c);
  }
}
addDependent(Container con){
  for(Container c : dependencies){
    if(c.id == con.id) return;
  }
  dependencies.add(c);
}