簡體   English   中英

如果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建議),如果沒有同步或在TestMap對象上進行同步,則不會顯示ConcurrentModificationException。 我想我現在已經了解了這個問題。 非常歡迎對此提出任何雙重確認或建議。 再次非常感謝。

基於已添加(出於任何原因同時又將其刪除)的代碼的一些常規語句。

編輯另請參閱答案底部的更新

你應該知道什么 ,你實際上是同步的。 當您使用創建地圖時

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 明確設計為可同時使用,並在內部進行所有必要的同步-特別是,它在內部使用一些技巧來避免“大”同步塊。 在問題中當前顯示的代碼中,映射將在迭代時被“鎖定”,並且其他線程將無法對映射進行任何修改,這可能會對整體性能產生負面影響。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM