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

Will Collections.synchronizedMap be thread safe if synchronized while iteration?

我在迭代 Collections.synchronizedMap 时尝试修复与 ConcurrentModificationException 相关的错误。

按照Javadoc的要求,地图上的迭代过程已经同步。

我检查了迭代过程,地图的大小没有明显的变化(方法调用trace很长,我会仔细检查)。

除了迭代过程中的修改,还有没有其他可能导致这个异常的可能?

由于迭代已经同步,据我了解,其他线程将无法进行 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 对象上同步,则显示异常。我想我现在明白这个问题了。非常欢迎任何关于此的双重确认或建议。再次感谢。

一些一般性陈述,基于已添加的代码(同时出于某种原因再次删除)。

EDIT Also see the update at the bottom of the answer

您应该知道您实际同步的是什么。当您使用

创建地图时
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 类似于以下内容:

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())
{
    ...
}

...但这不是一般性建议,但只是为了传达这个想法:你必须知道对象是什么你必须同步。对于实际应用,很可能有更优雅的解决方案。

Update:

再说一句,现在又把"real"代码加到问题里了:你也可以考虑ConcurrentHashMap。它 明确设计 并发使用,并在内部执行所有必要的同步 - 特别是,它在内部使用一些技巧来避免 "large" 同步块。在问题中当前显示的代码中,地图在迭代时将是 "locked",并且没有其他线程能够对地图进行任何修改,这可能会对整体性能产生负面影响。