我目前正在开发一个多线程应用程序,偶尔也会收到同时修改的异常(平均大约每小时一次或两次,但是看似随机的间隔)。
错误的类本质上是地图的包装器 - 它扩展了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,请执行以下任一操作改变顺序(即计入“访问”)吗
答案 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)
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指定:“在访问顺序链接哈希映射中,仅使用获取查询地图是一种结构修改。”