并发修改例外
我目前正在研究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? 你重写了HashMap
的keySet()
方法吗?
编辑我的错误 – 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查询映射是一种结构修改。”