Как мне синхронизировать, чтобы предотвратить исключение java.util.ConcurrentModificationException - PullRequest
1 голос
/ 19 декабря 2009

У меня есть программа, состоящая из нескольких классов. У меня проблема с взаимодействием двух классов - WebDataCache и Client. Классы проблем перечислены ниже.

WebData:
Это просто класс данных, представляющий некоторые данные, полученные из Интернета.
WebService:
Это просто класс-оболочка веб-службы, который подключается к определенной веб-службе, считывает некоторые данные и сохраняет их в объекте типа WebData.
WebDataCache:
Это класс, который использует класс WebService для извлечения данных, которые кэшируются на карте, на основе полей идентификаторов данных.
Клиент:
Это класс, который содержит ссылку на экземпляр класса WebDataCache и использует кэшированные данные.

Проблема заключается в том (как показано ниже), когда класс циклически перебирает кэшированные данные, возможно, WebDataCache обновляет базовую коллекцию.

У меня вопрос, как мне синхронизировать доступ к кешу?

Я не хочу синхронизировать весь кэш, поскольку существует несколько экземпляров класса Client, однако каждый экземпляр создается с уникальным идентификатором (т. Е. New Client (0, ...), new Client (1, ... ), новый клиент (2, ...) и т. д. каждый экземпляр заинтересован только в данных, идентифицированных идентификатором, с которым был создан клиент.

Есть ли подходящие шаблоны проектирования, которые я могу использовать?

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 ]

5 голосов
/ 19 декабря 2009

Используйте 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>>();
3 голосов
/ 19 декабря 2009

Моя лучшая рекомендация - использовать существующую реализацию кеша, такую ​​как JCS или EhCache - это проверенные в бою реализации.

В противном случае в вашем коде происходит несколько вещей. Вещи, которые могут ломаться забавными способами.

  • HashMap может увеличивать бесконечные циклы при одновременном изменении несколькими потоками. Так что не надо. Вместо этого используйте java.util.concurrent.ConcurrentHashMap.
  • ArrayList, который вы используете для WebDataCache.requests, также не является поточно-ориентированным, и у вас есть противоречивая синхронизация - либо измените его на более безопасную реализацию списка с java.util.concurrent, либо убедитесь, что all имеет доступ он синхронизируется с таким же замком.
  • Наконец, ваш код проверен с помощью FindBugs и / или должным образом проверен кем-то, обладающим глубокими знаниями и опытом написания многопоточного кода.

Если вы хотите почитать книгу об этом материале, я могу порекомендовать вам параллелизм Java на практике Брайана Гетца.

2 голосов
/ 19 декабря 2009

В дополнение к другим опубликованным рекомендациям, подумайте, как часто обновляется кэш, а не просто читается. Если чтение преобладает, а обновление происходит редко, и не важно, чтобы цикл чтения мог видеть каждое обновление немедленно, рассмотрите возможность использования CopyOnWriteArraySet. Это и его родной брат CopyOnWriteArrayList позволяют одновременное чтение и обновление членов; читатель видит непротиворечивый снимок, на который не влияет ни одна мутация базовой коллекции - аналогично уровню изоляции SERIALIZABLE в реляционной базе данных.

Проблема, однако, в том, что ни одна из этих двух структур не предоставляет вам словарь или хранилище ассоциативных массивов (а-ля Map) из коробки. Вам нужно определить составную структуру для хранения ключа и значения вместе, и, учитывая, что CopyOnWriteArraySet использует Object#equals() для тестирования членства, вам придется написать нетрадиционный метод equals() на основе ключа для вашей структуры. .

1 голос
/ 19 декабря 2009

Ответ от LES2 хорош, за исключением того, что вам придется заменить:

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

на:

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

Поскольку это тот, который содержит«проблемная» коллекция а не весь кеш.

0 голосов
/ 19 декабря 2009

Вы можете выполнить синхронизацию с 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 в кеше и итерации на клиенте.

Кстати, почему у вас есть карта в кэше, и вы используете коллекцию в клиенте. Вы должны использовать одну и ту же структуру на обоих и не раскрывать основную реализацию.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...