我正在尝试在迭代ConcurrentModificationException
时修复与Collections.synchronizedMap
相关的错误。
根据Javadoc的要求,迭代过程已在地图上同步。
我检查了迭代过程,对地图的大小没有明显的修改(调用跟踪的方法很长,我会仔细检查)。
除了迭代过程中的修改外,还有其他可能导致此异常的可能性吗?
由于迭代已经同步,根据我的理解,其他线程将无法进行add()
或remove()
等修改,这是对的吗?
我对这些东西真的很陌生。任何帮助都将非常感激。
更新
非常感谢大家的帮助,特别是来自@ Marco13的详细解释。我做了一个小代码来测试和验证这个问题,代码附在这里:
public class TestCME {
public static void main(String[] args){
TestMap tm = new TestMap();
for(int i = 0;i < 50;i++){
tm.addCity(i,new City(i * 10));
}
RunnableA rA = new RunnableA(tm);
new Thread(rA).start();
RunnableB rB = new RunnableB(tm);
new Thread(rB).start();
}
}
class TestMap{
Map<Integer,City> cityMap;
public TestMap(){
cityMap = Collections.synchronizedMap(new HashMap<Integer,City>());
}
public Set<Integer> getAllKeys(){
return cityMap.keySet();
}
public City getCity(int id){
return cityMap.get(id);
}
public void addCity(int id,City city){
cityMap.put(id,city);
}
public void removeCity(int id){
cityMap.remove(id);
}
}
class City{
int area;
public City(int area){
this.area = area;
}
}
class RunnableA implements Runnable{
TestMap tm;
public RunnableA(TestMap tm){
this.tm = tm;
}
public void run(){
System.out.println("Thread A is starting to run......");
if(tm != null && tm.cityMap != null && tm.cityMap.size() > 0){
synchronized (tm.cityMap){
Set<Integer> idSet = tm.getAllKeys();
Iterator<Integer> itr = idSet.iterator();
while(itr.hasNext()){
System.out.println("Entering while loop.....");
Integer id = itr.next();
System.out.println(tm.getCity(id).area);
try{
Thread.sleep(100);
}catch(Exception e){
e.printStackTrace();
}
}
}
/*Set<Integer> idSet = tm.getAllKeys();
Iterator<Integer> itr = idSet.iterator();
while(itr.hasNext()){
System.out.println("Entering while loop.....");
Integer id = itr.next();
System.out.println(tm.getCity(id).area);
try{
Thread.sleep(100);
}catch(Exception e){
e.printStackTrace();
}
}*/
}
}
}
class RunnableB implements Runnable{
TestMap tm;
public RunnableB(TestMap tm){
this.tm = tm;
}
public void run(){
System.out.println("Thread B is starting to run......");
System.out.println("Trying to add elements to map....");
tm.addCity(50,new City(500));
System.out.println("Trying to remove elements from map....");
tm.removeCity(1);
}
}
我试图恢复我的错误,所以代码有点冗长,我很抱歉。在线程A中,我在地图上进行迭代,而在线程B中我正在尝试添加和删除地图中的元素。通过地图上的正确同步(如@ Marco13建议的那样),我不会看到ConcurrentModificationException,如果没有同步或在TestMap对象上同步,则会显示异常。我想我现在明白这个问题了。任何双重确认或建议都非常受欢迎。再次感谢。
答案 0 :(得分:4)
一些一般性陈述,基于已添加的代码(在此期间,无论出于何种原因再次删除)。
编辑另请参阅答案底部的更新
您应该了解实际正在同步的。使用
创建地图时Map<Key, Value> synchronizedMap =
Collections.synchronizedMap(map);
然后同步将在synchronizedMap
上进行。这意味着以下是线程安全的:
void executedInFirstThread()
{
synchronizedMap.put(someKey, someValue);
}
void executedInSecondThread()
{
synchronizedMap.remove(someOtherKey);
}
虽然两个方法可以由不同的线程同时执行,但它是线程安全的:当第一个线程执行put
方法时,第二个线程必须等待在执行remove
方法之前。 put
和remove
方法将永远不会同时执行。这就是同步的目的。
然而,ConcurrentModificationException
的含义更广泛。关于特定情况,并略微简化:ConcurrentModificationException
表示地图已修改,而正在对地图进行迭代。
因此,必须同步整个迭代,而不仅仅是单个方法:
void executedInFirstThread()
{
synchronized (synchronizedMap)
{
for (Key key : synchronizedMap.keySet())
{
System.out.println(synchronizedMap.get(key));
}
}
}
void executedInSecondThread()
{
synchronizedMap.put(someKey, someValue);
}
没有synchronized
块,第一个线程可以部分遍历地图,然后,在迭代过程中,第二个线程可以调用put
地图(因此,执行并发修改)。当第一个线程想要继续迭代时,将抛出ConcurrentModificationException
。
使用synchronized
块,这不可能发生:当第一个线程进入此synchronized
块时,第二个线程必须等待它才能调用put
。 (它必须等到第一个线程离开synchronized
块。之后,它可以安全地进行修改)。
但请注意,整个概念始终指的是在上同步的对象。
在您的情况下,您有一个类似于以下的类:
class TestMap
{
Map<Key, Value> synchronizedMap =
Collections.synchronizedMap(new HashMap<Key, Value>());
public Set<Key> getAllKeys()
{
return synchronizedMap.keySet();
}
}
然后,你就这样使用它了:
TestMap testMap = new TestMap();
synchronized(testMap)
{
Set<Key> keys = testMap.getAllKeys();
for (Key key : keys)
{
...
}
}
}
在这种情况下,您正在同步testMap
对象,并且 在其包含的sychronizedMap
对象上。因此,迭代不会发生同步。一个线程可以进行迭代(在testMap
对象上同步)。同时,不同的线程可以(同时)修改synchronizedMap
,这将导致ConcurrentModificationException
。
解决方案取决于整体结构。一种可能的解决方案是公开synchronizedMap
...
class TestMap
{
Map<Key, Value> synchronizedMap =
Collections.synchronizedMap(new HashMap<Key, Value>());
...
public Object getSynchronizationMonitor()
{
return synchronizedMap;
}
}
并将其用于迭代的同步......
TestMap testMap = new TestMap();
synchronized(testMap.getSynchronizationMonitor())
{
...
}
...但不是一般性推荐,但仅用于表达想法:您必须知道要同步的对象。对于实际应用,最有可能是更优雅的解决方案。
更新
另一个评论,现在再次将“真实”代码添加到问题中:您还可以考虑ConcurrentHashMap
。 明确设计以便同时使用,并在内部进行所有必要的同步 - 特别是,它在内部使用一些技巧来避免“大”同步块。在当前显示在问题中的代码中,地图将在迭代时被“锁定”,并且没有其他线程可以对地图进行任何修改,这可能会对整体性能产生负面影响。