如何同步以防止java.util.ConcurrentModificationException

时间:2009-12-18 23:04:44

标签: java concurrency synchronization

我的程序由许多类组成。我有两个类的交互问题 - WebDataCache和Client。问题类列在下面。

WEBDATA:
这只是一个数据类,表示从互联网上检索到的一些数据 的 WebService的:
这只是一个Web服务包装类,它连接到特定的Web服务,读取一些数据并将其存储在WebData类型的对象中。
WebDataCache:
这是一个使用WebService类来检索缓存在地图中的数据的类,该数据由数据的ids字段键入。
客户端:
这是一个包含对WebDataCache类实例的引用并使用缓存数据的类。

问题是(如下图所示)当类循环缓存数据时,WebDataCache可以更新底层集合。

我的问题是如何同步对缓存的访问?

我不想同步整个缓存,因为Client类有多个实例,但每个实例都有一个唯一的id(即新的Client(0,...),new Client(1,... ),新客户端(2,...)等每个实例仅对客户端所实现的id所键入的数据感兴趣。

我可以使用任何相关的设计模式吗?

class WebData {
    private final int id;
    private final long id2;

    public WebData(int id, long id2) {
        this.id = id;
        this.id2 = id2;
    }

    public int getId() { return this.id; }

    public long getId2() { return this.id2; }
}

class WebService {
    Collection<WebData> getData(int id) {
        Collection<WebData> a = new ArrayList<WebData>();
        // populate A with data from a webservice
        return a;
    }
}

class WebDataCache implements Runnable {
    private Map<Integer, Map<Long, WebData>> cache =
        new HashMap<Integer, Map<Long, WebData>>();
    private Collection<Integer> requests =
        new ArrayList<Integer>();

    @Override
    public void run() {
        WebService webSvc = new WebService();
        // get data from some web service
        while(true) {
            for (int id : requests) {
                Collection<WebData> webData = webSvc.getData(id);
                Map<Long, WebData> row = cache.get(id);

                if (row == null)
                    row = cache.put(id, new HashMap<Long, WebData>());
                else
                    row.clear();

                for (WebData webDataItem : webData) {

                    row.put(webDataItem.getId2(), webDataItem);
                }
            }
            Thread.sleep(2000);
        }
    }

    public synchronized Collection<WebData> getData(int id){
        return cache.get(id).values();
    }

    public synchronized void requestData(int id) {
        requests.add(id);
    }
}

-

class Client implements Runnable {
    private final WebDataCache cache;
    private final int id;

    public Client(int id, WebDataCache cache){
        this.id = id;
        this.cache = cache;
    }
    @Override
    public void run() {

        cache.requestData(id);

        while (true) {


            for (WebData item : cache.getData(id)) {
            // java.util.ConcurrentModificationException is thrown here...
            // I understand that the collection is probably being modified in WebDataCache::run()
            // my question what's the best way to sychronize this code snippet?
            }
        }
    }
}

谢谢!

5 个答案:

答案 0 :(得分:5)

使用java.util.concurrent.ConcurrentHashMap而不是普通的旧java.util.HashMap。来自Javadoc:

  

支持完整的哈希表   检索和并发   可调节的预期并发性   更新。这门课也服从同样的道理   功能规范为Hashtable,   并包括方法的版本   对应于每种方法   哈希表。但是,尽管如此   操作是线程安全的,检索   操作不需要锁定,并且   没有任何锁定支持   整个表格的方式   阻止所有访问。这堂课是   与Hashtable完全互操作   依赖于其线程的程序   安全但不同步   的信息。

http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/ConcurrentHashMap.html

所以你要替换:

private Map<Integer, Map<Long, WebData>> cache =
    new HashMap<Integer, Map<Long, WebData>>();

使用

private Map<Integer, Map<Long, WebData>> cache =
    new ConcurrentHashMap<Integer, Map<Long, WebData>>();

答案 1 :(得分:3)

我最好的建议是使用现有的缓存实施,例如JCSEhCache - 这些是经过实战考验的实施。

否则,您的代码中会发生一些事情。可能会以有趣的方式打破的事情。

  • 当多个线程同时修改时,HashMap可以增长无限循环。所以不要。请改用java.util.concurrent.ConcurrentHashMap
  • 您用于WebDataCache.requests的ArrayList也不是线程安全的,并且您的同步不一致 - 要么将其更改为java.util.concurrent中更安全的列表实现,要么确保 all 访问它正在同步相同的锁。
  • 最后,请使用FindBugs检查您的代码和/或由具有编写多线程代码的丰富知识和经验的人员进行适当审核。

如果你想阅读这本书的书,我可以推荐Brian Goetz的Java Concurrency in Practice。

答案 2 :(得分:2)

除了其他发布的建议之外,还要考虑缓存更新的频率与刚刚读取的频率。如果读数占主导地位并且更新很少,并且读取循环能够立即看到每个更新并不重要,请考虑使用CopyOnWriteArraySet。它和它的兄弟CopyOnWriteArrayList允许同时阅读和更新成员;读者可以看到一致的快照,不受基础集合任何突变的影响 - 类似于关系数据库中的 SERIALIZABLE 隔离级别。

这里的问题是,这两个结构都没有为您提供开箱即用的字典或关联数组存储(la Map)。您必须定义一个复合结构来将键和值存储在一起,并且,鉴于CopyOnWriteArraySet使用Object#equals()进行成员资格测试,您必须编写一个非常规的基于密钥的{{1你的结构的方法。

答案 3 :(得分:1)

LES2的答案很好,除非您必须替换:

 row = cache.put(id, new HashMap<Long, WebData>());

使用:

row = cache.put(id, new ConcurrentHashMap<Long, WebData>());

对于那个持有“有问题”集合而不是整个缓存的那个。

答案 4 :(得分:0)

您可以对最终拥有正在共享的集合的缓存返回的row进行同步。

在WebDataCache上:

            Map<Long, WebData> row = cache.get(id);

            if (row == null) {
                row = cache.put(id, new HashMap<Long, WebData>());
             } else synchronized( row ) {
                row.clear();
             }

            for (WebData webDataItem : webData) synchronized( row ) {

                row.put(webDataItem.getId2(), webDataItem);
            }

           // it doesn't make sense to synchronize the whole cache here. 
           public Collection<WebData> getData(int id){
               return cache.get(id).values();
           }

在客户端:

         Collection<WebData>  data = cache.getData(id);
         synchronized( data ) {
             for (WebData item : cache.getData(id)) {
             }
         }

当然,这远非完美,只是回答了要同步的问题。在这种情况下,可以访问缓存中的row.clear row.put中的底层集合以及客户端上的迭代。

顺便说一句,为什么你在缓存中有一个Map,你在客户端使用一个集合。您应该在两者上使用相同的结构,并且不要公开底层实现。