并发修改例外

我目前正在研究multithreading应用程序,偶尔也会收到同时修改的exception(平均大约每小时一次或两次,但是看似随机的间隔)。

有缺陷的类本质上是地图的包装器 – 它扩展了LinkedHashMap (将accessOrder设置为true)。 该类有几个方法:

 synchronized set(SomeKey key, SomeValue val) 

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

 synchronized get(SomeKey key) 

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

 rebuild() 

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

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

例外情况发生在

 keysCopy.addAll(internalMap.keySet()); 

在同步块内。

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

更新1:

消毒堆栈跟踪:

 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 abcetc.SomeWrapper.rebuild(SomeWraper.java:109) at abcetc.SomeCaller.updateCache(SomeCaller.java:421) ... 

更新2:

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

如果LinkedHashMap上的accessOrder设置为true,并且我访问其keySet然后继续通过addAll将keySet添加到linkedList,这些操作中的任何一个是否会改变顺序(即计入“访问”)?

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

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

例如:

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

足以在某些集合上获得exception(例如ArrayList)。

詹姆士

这些都是你的包装中的function吗? 因为当你以某种方式在另一个地方迭代集合时,可能会抛出此exception。 而且我猜你的方法与潜在的明显竞争条件同步,但可能错过了不太明显的情况。 这里是对exception类docs的引用。

来自Javadoc:

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

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

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

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

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

这很奇怪。 我无法看到在您识别的位置抛出exception的任何方式(在Java 6的默认实现中)。 你在ArrayList或HashMap中得到ConcurrentModificationException? 你重写了HashMapkeySet()方法吗?

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

代码中的某个位置允许您更新未同步的内部映射。

  • 你有一个实用方法暴露你的内部地图“不应该使用”或
  • 是您的内部地图被确定为公共范围

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

尝试将Map声明为瞬态

在迭代键时,由于

 keysCopy.addAll(internalMap.keySet()); 

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

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

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

你没有尝试使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 。 如果你确定set() / get() / rebuild()是直接访问internalMap的唯一方法,并且它们都是以同步方式访问它。

 private volatile Map internalMap ... 

尝试这个:

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

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

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