并发修改例外

时间:2009-07-06 22:44:47

标签: java collections concurrency

我目前正在开发一个多线程应用程序,偶尔也会收到同时修改的异常(平均大约每小时一次或两次,但是看似随机的间隔)。

错误的类本质上是地图的包装器 - 它扩展了LinkedHashMap(将accessOrder设置为true)。该课程有几种方法:

synchronized set(SomeKey key, SomeValue val)

set方法将键/值对添加到内部映射,并受synchronized关键字的保护。

synchronized get(SomeKey key)

get方法根据输入键返回值。

rebuild()

内部地图偶尔重建一次(〜每2分钟,间隔与异常不匹配)。 rebuild方法基本上根据键重建值。由于rebuild()相当昂贵,我没有在方法上放置synchronized关键字。相反,我正在做:

public void rebuild(){
  /* initialization stuff */
  List<SomeKey> keysCopy = new ArrayList<SomeKey>();
  synchronized (this) {
    keysCopy.addAll(internalMap.keySet());
  }
  /* 
    do stuff with keysCopy, update a temporary map
   */    
  synchronized (this) {
    internalMap.putAll(tempMap);
  }
}

异常发生在

keysCopy.addAll(internalMap.keySet());

在synchronized块内。

非常感谢您的建议。请随意指出 Effective Java 和/或实践中的并发中的特定页面/章节。

更新1:

Sanitized stacktrace:

java.util.ConcurrentModificationException
        at java.util.LinkedHashMap$LinkedHashIterator.nextEntry(LinkedHashMap.java:365)
        at java.util.LinkedHashMap$KeyIterator.next(LinkedHashMap.java:376)
        at java.util.AbstractCollection.toArray(AbstractCollection.java:126)
        at java.util.ArrayList.addAll(ArrayList.java:473)
        at a.b.c.etc.SomeWrapper.rebuild(SomeWraper.java:109)
        at a.b.c.etc.SomeCaller.updateCache(SomeCaller.java:421)
        ...

更新2:

感谢大家到目前为止的答案。我认为问题在于LinkedHashMap及其accessOrder属性,尽管我并不完全确定atm(调查)。

如果LinkedHashMap上的accessOrder设置为true,并且我访问其keySet然后继续通过addAll将keySet添加到linkedList,请执行以下任一操作改变顺序(即计入“访问”)吗

12 个答案:

答案 0 :(得分:7)

如果使用accessOrder = true构造LinkedHashMap,则LinkedHashMap.get()实际上会改变LinkedHashMap,因为它将最近访问的条目存储在条目链接列表的前面。当数组列表使用Iterator进行复制时,可能会调用get()。

答案 1 :(得分:6)

此异常通常与同步无关 - 如果在Iterator迭代时修改了Collection,通常会抛出此异常。 AddAll方法可能使用迭代器 - 值得注意的是,对于Iterator的实例,posh foreach循环也会迭代。

e.g:

for(Object o : objects) {
    objects.remove(o);
}

足以在某些集合上获取异常(例如ArrayList)。

詹姆斯

答案 2 :(得分:2)

你的包装中是否包含所有功能?因为当你以某种方式在另一个地方迭代集合时,可能抛出此异常。而且我猜你的方法与潜在的明显竞争条件同步,但可能错过了不太明显的情况。 Here对异常类文档的引用。

答案 3 :(得分:1)

来自Javadoc:

  

如果多个线程同时访问链接的哈希映射,并且至少有一个线程在结构上修改了映射,则必须在外部进行同步。这通常通过在自然封装地图的某个对象上进行同步来实现。如果不存在此类对象,则应使用Collections.synchronizedMap方法“包装”该映射。这最好在创建时完成,以防止意外地不同步访问地图:

     

Map m = Collections.synchronizedMap(new LinkedHashMap(...));

实际包装LinkedHashMap可能更安全,而不是声称你扩展它。所以你的实现将有一个内部数据成员,它是Collections.synchronizedMap返回的Map(new LinkedHashMap(...))。

有关详细信息,请参阅集合javadoc:Collections.synchronizedMap

答案 4 :(得分:0)

尝试从set()和get()方法中删除synchronized关键字,而是使用方法内部的synchronized块,锁定internalMap;然后更改rebuild()方法上的synchronized块以锁定internalMap。

答案 5 :(得分:0)

这很奇怪。我无法看到在您识别的位置抛出异常的任何方式(在Java 6的默认实现中)。你在ArrayList或HashMap中得到ConcurrentModificationException?您是否覆盖了keySet()中的HashMap方法?

编辑我的错误 - ArrayList将强制KeySet迭代(AbstractCollection.toArray()将迭代其键)

代码中有某处允许您更新未同步的内部地图。

  • 你有一个公开你的内部地图“不应该使用”或
  • 的实用工具方法
  • 是您的内部地图,标识为公共范围

答案 6 :(得分:0)

internalMap是静态的吗?您可能有多个对象,每个对象都锁定在this对象上,但没有在静态internalMap上提供正确的锁定。

答案 7 :(得分:0)

我认为您的set() / get()rebuild()所在的同一监视器不同步。这使得有人可以在执行有问题的行时调用set / get,特别是在addAll()调用期间(通过堆栈跟踪公开的内部实现)迭代internalMap的键集时。 p>

不是让set()同步,而是尝试了以下方面的内容:

public void set(SomeKey key, SomeValue val) {
    synchronized(this) {
        internalMap.put(key, val); // or whatever your get looks like
    }
}

我认为您根本不需要同步get(),但如果您坚持:

public SomeValue get(SomeKey key) {
    synchronized(this) {
        internalMap.get(key); // or whatever your get looks like
    }
}

事实上,我认为最好不要与internalMap而不是this同步。制作internalMap volatile也没有什么坏处,但如果您确信set() / get() / {{1}我认为这不是必要的是唯一直接访问internalMap的方法,它们都是以同步的方式访问它。

rebuild()

答案 8 :(得分:0)

在迭代键时,由于

keysCopy.addAll(internalMap.keySet());

您是否认为某些条目可以从LinkedHashMap中删除?

removeEldestEntry方法可以返回true,也可以修改地图,从而扰乱迭代。

答案 9 :(得分:0)

尝试将Map声明为瞬态

答案 10 :(得分:0)

试试这个:

public void rebuild(){
  /* initialization stuff */
  List<SomeKey> keysCopy = new ArrayList<SomeKey>();
  synchronized (internalMap) {
    keysCopy.addAll(internalMap.keySet());
  }
  /* 
    do stuff with keysCopy, update a temporary map
   */    
  synchronized (internalMap) {
    internalMap.putAll(tempMap);
  }
}

答案 11 :(得分:0)

继续访问internalMap syncronized ,否则会发生java.util.ConcurrentModificationException,因为在键集迭代期间可以同时更改HashMap#modCount(记录结构更改)(由于keysCopy.addAll(internalMap.keySet( )调用)。

LinkedHashMap javaDoc指定:“在访问顺序链接哈希映射中,仅使用获取查询地图是一种结构修改。”