2016-09-13 2 views
0

У меня есть 1,8 класс Java, который содержит две коллекции:методы Синхронизировать для предотвращения ConcurrentModificationException

Map<Key,Object> 
Set<Object> 

Мой класс пять методов:

addObjectToMap() 
removeObjectFromMap() 
addObjectToSet() 
removeObjectFromSet() 

loopOverEverything(){ 
    for(Object o : mySet){ 
     for(Object o2 : myMap.getKeySet()){ 
      doSomething(o,o2); 
     } 
    } 
} 

Точка класса реализовать шаблон наблюдателя, но быть очень гибкими как в наблюдателях, так и в наблюдениях. Проблема, с которой я сталкиваюсь, заключается в том, что последний метод может легко вызывать исключение ConcurrentModificationException, когда поток вызывает методы add/remove во время цикла. Я думал о синхронизации всех методов на «это»:

set.add(object); 

станет

synchronized(this){ 
    set.add(object); 
} 

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

Будет ли это работать? Я знаю, что методы синхронизации могут вызвать узкие места. Несмотря на то, что в настоящее время нет проблемы с производительностью, я хотел бы рационализировать этот дизайн и узнать о возможных альтернативах, которые имеют меньшую производительность.

+1

@AndyTurner Это, вероятно, не решит проблему - 'Collections.synchronizedSet' не имеет блокировки во время итерации, поэтому вы все равно можете легко получить' ConcurrentModificationException'. – Sbodd

ответ

1

Поскольку блокировки являются повторителями, синхронизация доступа к коллекции не останавливает выполнение одного и того же потока по коллекции, а затем изменяет сама коллекция. Несмотря на название, ConcurrentModificationException не всегда поднимается из-за одновременной модификации другим потоком. Регистрация или отказ от регистрации других наблюдателей в обратном вызове уведомления является главным виновником такой параллельной модификации.

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

Чтобы сделать снимок, вы можете скопировать наблюдатель на временную коллекцию или массив:

Collection<?> observers = new ArrayList<>(mySet); 

или

Object[] observers = mySet.toArray(new Object[mySet.size()]); 

Тогда итерации по этой копии, оставляя оригинал для обновления:

for (Object o : observers) { 
    ... 
    doSomething(o, ...); 
    ... 
} 

Некоторые параллельные коллекции, такие как ConcurrentSkipListSet, не будут вызывать исключение , но они только гарантируют «слабо согласованную» итерацию. Это может привести к тому, что некоторые (вновь добавленные) наблюдатели неожиданно получат текущее уведомление. CopyOnWriteArraySet использует технику моментальной съемки. Если вы редко модифицируете набор, он будет более эффективным, потому что он только копирует массив, когда это необходимо.

+0

[У этого сообщения есть несколько хороших примеров] (http://stackoverflow.com/a/28930647/3474) слабо согласованной итерации. – erickson

+0

Я понимаю, почему один поток может теоретически одновременно обращаться к методам добавления/удаления и методам цикла, но я не понимаю, как один поток, который выполняется последовательно над кодом, может выполнить оба. Например, когда он добавляет объект objec, он не может быть циклическим, потому что это единственный поток. Может ли один поток «расщепляться» процессором/ядрами одновременно выполнять разные задачи? – user1884155

+0

@ user1884155 Нет. Это не так сложно. Это всего лишь цикл, который изменяет коллекцию. Как и в вашем исходном примере, предположим, что ваш метод 'doSomething()' добавляет элемент в 'mySet'. Это может вызвать «ConcurrentModificationException». Поток выполняет только одно действие за раз: проверьте, есть ли другой элемент; если да, то вперед; затем обработать его; затем повторите. Проблема в том, что если «обработать это» приводит к изменению коллекции, «проверка, чтобы увидеть, есть ли другой элемент», который происходит в следующем повторе, обнаружит, что коллекция изменилась и вызвала исключение. – erickson

0

Один из возможных ответов, который будет работать лучше, чем использование блоков synchronized, - это изменение ваших коллекций в потокобезопасных. Вместо HashMap (или любую другую карту, которую вы используете) используйте ConcurrentHashMap. Замените Set либо CopyOnWriteArraySet, либо ConcurrentSkipListSet. Вам нужно будет прочитать документацию по этим вопросам, чтобы убедиться, что их поведение в потоке - это то, что вы на самом деле хотите, но оба должны быть улучшены по сравнению с небезопасным классом без синхронизации.

2

Нет, это небезопасно, если петля также не синхронизирована. Если вы хотите избежать накладных расходов на блокировку в течение всего цикла, рассмотрите возможность использования параллельных коллекций, таких как ConcurrentHashMap и Collections.newSetFromMap(new ConcurrentHashMap<>()).

+0

Вы говорите, что одновременный хэш-файл также блокирует итерацию по набору ключей этой карты? – user1884155

+0

@ user1884155 Он не блокируется, но гарантируется, что он будет потокобезопасным. Подробнее см. В документации. – shmosel

+0

Как интерпретировать это сообщение в документации: «Они не выбрасывают ConcurrentModificationException. Однако итераторы предназначены для использования только по одному потоку за раз». Что они подразумевают под «предназначены для»? Если я удаляю операторы синхронизации в методе looping, но использую concurrenthashmap, что произойдет, если два потока начнут итерацию одновременно? – user1884155