如果在迭代时同步,Collections.synchronizedMap是否是线程安全的?

时间:2015-09-04 13:47:45

标签: java synchronized

我正在尝试在迭代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对象上同步,则会显示异常。我想我现在明白这个问题了。任何双重确认或建议都非常受欢迎。再次感谢。

1 个答案:

答案 0 :(得分:4)

一些一般性陈述,基于已添加的代码(在此期间,无论出于何种原因再次删除)。

  

编辑另请参阅答案底部的更新

您应该了解实际正在同步的。使用

创建地图时
Map<Key, Value> synchronizedMap = 
    Collections.synchronizedMap(map);

然后同步将在synchronizedMap上进行。这意味着以下是线程安全的:

void executedInFirstThread() 
{
    synchronizedMap.put(someKey, someValue);
}
void executedInSecondThread() 
{
    synchronizedMap.remove(someOtherKey);
}

虽然两个方法可以由不同的线程同时执行,但它是线程安全的:当第一个线程执行put方法时,第二个线程必须等待在执行remove方法之前。 putremove方法将永远不会同时执行。这就是同步的目的。

然而,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明确设计以便同时使用,并在内部进行所有必要的同步 - 特别是,它在内部使用一些技巧来避免“大”同步块。在当前显示在问题中的代码中,地图将在迭代时被“锁定”,并且没有其他线程可以对地图进行任何修改,这可能会对整体性能产生负面影响。