有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

并发性如何进行同步以防止java冲突。util。ConcurrentModificationException

我有一个由许多课程组成的课程。我对其中两个类(WebDataCache和Client)的交互有问题。下面列出了问题类

网络数据:
这只是一个数据类,表示从internet检索到的一些数据
网络服务:
这只是一个web服务包装类,它连接到特定的web服务,读取一些数据并将其存储在WebData类型的对象中
WebDataCache:
这是一个使用WebService类检索缓存在映射中的数据的类,由数据的ID字段设置关键字
客户端:
这是一个包含对WebDataCache类实例的引用并使用缓存数据的类

问题是(如下所示),当类在缓存数据中循环时,WebDataCache可能会更新基础集合

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

我不想同步整个缓存,因为客户端类有多个实例,但是每个实例都用唯一的id实例化(即新客户端(0,…),新客户(1,…),新客户(2,…),etc每个实例只对客户机实例所用的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) 个答案

  1. # 1 楼答案

    来自LES2的答案很好,只是您必须替换:

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

    与:

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

    因为它保存的是“有问题”的集合,而不是整个缓存

  2. # 2 楼答案

    我的最佳建议是使用现有的缓存实现,如JCSEhCache——这些都是经过战斗测试的实现

    否则,代码中会出现一些问题。可以以有趣的方式打破的东西

    • 当由多个线程并发修改时,HashMap可以生成无限循环。所以不要。改用java.util.concurrent.ConcurrentHashMap
    • 用于WebDataCache的ArrayList。请求也不是线程安全的,并且同步不一致-请将其从java.util.concurrent更改为更安全的列表实现,或者确保对它的所有访问都在相同的锁上同步
    • 最后,让FindBugs检查您的代码和/或让具有编写多线程代码的扎实知识和经验的人适当地检查代码

    如果你想读一本关于这方面的书,我可以推荐Brian Goetz的《Java并发实践》

  3. # 3 楼答案

    使用java。util。同时发生的ConcurrentHashMap代替了普通的旧java。util。哈希映射。从Javadoc:

    A hash table supporting full concurrency of retrievals and adjustable expected concurrency for updates. This class obeys the same functional specification as Hashtable, and includes versions of methods corresponding to each method of Hashtable. However, even though all operations are thread-safe, retrieval operations do not entail locking, and there is not any support for locking the entire table in a way that prevents all access. This class is fully interoperable with Hashtable in programs that rely on its thread safety but not on its synchronization details.

    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>>();
    
  4. # 4 楼答案

    您可以在缓存返回的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.put和客户端上的迭代

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

  5. # 5 楼答案

    除了其他发布的建议之外,还要考虑缓存的更新与读取的频繁程度。如果阅读占主导地位,更新是罕见的,并不是关键的阅读循环能够看到每一个更新立即,考虑使用^{}。它及其同级^{}允许同时读取和更新成员;读者可以看到一个一致的快照,它不受底层集合的任何变化的影响——类似于关系数据库中的可序列化隔离级别

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