Почему it.next () выбрасывает исключение java.util.ConcurrentModificationException? - PullRequest
10 голосов
/ 30 августа 2011
final Multimap<Term, BooleanClause> terms = getTerms(bq);
        for (Term t : terms.keySet()) {
            Collection<BooleanClause> C = new HashSet(terms.get(t));
            if (!C.isEmpty()) {
                for (Iterator<BooleanClause> it = C.iterator(); it.hasNext();) {
                    BooleanClause c = it.next();
                    if(c.isSomething()) C.remove(c);
                }
            }
        }

Не SSCCE, но вы можете уловить запах?

Ответы [ 3 ]

24 голосов
/ 30 августа 2011

Iterator для класса HashSet - это отказоустойчивый итератор. Из документации HashSet класс:

Итераторы, возвращаемые методом итератора этого класса, работают быстро: если набор изменяется в любое время после создания итератора, в любым способом, кроме как через собственный метод удаления итератора, итератор генерирует исключение ConcurrentModificationException. Таким образом, перед лицом одновременная модификация, итератор быстро и чисто дает сбой, вместо того, чтобы рисковать произвольным, недетерминированным поведением в неопределенное время в будущем.

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

Обратите внимание на последнее предложение - тот факт, что вы ловите ConcurrentModificationException, подразумевает, что другой поток модифицирует коллекцию. На той же странице API Javadoc также указано:

Если несколько потоков одновременно получают доступ к хэшу, и хотя бы один из потоков изменяет набор, он должен быть синхронизирован извне. Обычно это достигается путем синхронизации на некотором объекте, который естественно инкапсулирует набор. Если такого объекта не существует, множество должен быть "обернут", используя метод Collections.synchronizedSet . это лучше всего делать во время создания, чтобы предотвратить случайный несинхронизированный доступ к набору:

Set s = Collections.synchronizedSet(new HashSet(...));

Я считаю, что ссылки на Javadoc говорят сами за себя в том, что должно быть сделано дальше.

Кроме того, в вашем случае я не понимаю, почему вы не используете ImmutableSet вместо создания HashSet для объекта terms (который может быть изменен в промежуточный период; Я не вижу реализации метода getTerms, но у меня есть догадка, что базовый набор ключей изменяется). Создание неизменяемого набора позволит текущему потоку иметь собственную защитную копию исходного набора ключей.

Обратите внимание, что хотя ConcurrentModificationException можно предотвратить с помощью синхронизированного набора (как указано в документации по API Java), необходимо, чтобы все потоки обращались к синхронизированной коллекции, а не к резервной коллекции напрямую (что может быть неверно в вашем случае, поскольку HashSet, вероятно, создается в одном потоке, в то время как базовая коллекция для MultiMap модифицируется другими потоками). Классы синхронизированной коллекции фактически поддерживают внутренний мьютекс, к которому потоки могут получить доступ; поскольку вы не можете получить доступ к мьютексу напрямую из других потоков (и это было бы довольно нелепо здесь), вам следует рассмотреть возможность использования защитной копии либо набора ключей, либо самой MultiMap с использованием метода unmodifiableMultimap MultiMaps класса (вам нужно вернуть немодифицируемую MultiMap из метода getTerms). Вы также можете исследовать необходимость возврата синхронизированного MultiMap , но опять же, вам нужно будет убедиться, что мьютекс должен быть получен любым потоком, чтобы защитить базовую коллекцию от одновременных модификаций.

Обратите внимание, я намеренно не упомянул об использовании поточно-ориентированного HashSet по той единственной причине, что я не уверен в том, будет ли обеспечен одновременный доступ к фактической коллекции; скорее всего, это будет не так.


Редактировать: ConcurrentModificationException с Iterator.next в однопоточном сценарии

Это относится к утверждению: if(c.isSomething()) C.remove(c);, которое было введено в отредактированный вопрос.

Вызов Collection.remove меняет природу вопроса, поскольку теперь становится возможным выбросить ConcurrentModificationException даже в однопоточном сценарии.

Возможность возникает из-за использования самого метода в сочетании с использованием итератора Collection, в данном случае переменной it, которая была инициализирована с помощью оператора: Iterator<BooleanClause> it = C.iterator();.

Iterator it, который перебирает Collection C, сохраняет состояние, соответствующее текущему состоянию Collection.В этом конкретном случае (в предположении JRE Sun / Oracle) KeyIterator (внутренний внутренний класс HashMap класса, который используется HashSet) используется для итерации по Collection.Особенностью этого Iterator является то, что он отслеживает количество структурных изменений, выполненных на Collection (в данном случае HashMap) с помощью его метода Iterator.remove.

Когда вы вызываете remove на Collection напрямую, а затем с последующим вызовом Iterator.next, итератор выдает ConcurrentModificationException, так как Iterator.next проверяет, произошли ли какие-либо структурные изменения Collection, что Iteratorне знает о.В этом случае Collection.remove вызывает структурную модификацию, которая отслеживается Collection, но не Iterator.

Чтобы преодолеть эту часть проблемы, вы должны вызвать Iterator.remove ине Collection.remove, поскольку это гарантирует, что Iterator теперь знает об изменении Collection.Iterator в этом случае будет отслеживать структурные изменения, происходящие с помощью метода remove.Ваш код должен выглядеть следующим образом:

final Multimap<Term, BooleanClause> terms = getTerms(bq);
        for (Term t : terms.keySet()) {
            Collection<BooleanClause> C = new HashSet(terms.get(t));
            if (!C.isEmpty()) {
                for (Iterator<BooleanClause> it = C.iterator(); it.hasNext();) {
                    BooleanClause c = it.next();
                    if(c.isSomething()) it.remove(); // <-- invoke remove on the Iterator. Removes the element returned by it.next.
                }
            }
        }
8 голосов
/ 30 августа 2011

Причина в том, что вы пытаетесь изменить коллекцию вне итератора.

Как это работает:

Когда вы создаете итератор, коллекция поддерживает переменнуюificationNum как для коллекции, так и для итератора независимо. 1. Переменная для коллекции увеличивается на единицу для каждого изменения, вносимого в коллекцию и итератор. 2. Переменная для итератора увеличивается на единицу для каждого изменения, вносимого в итератор .

Так, когда вы вызываете it.remove() через итератор, который увеличивает значение переменной-числа-модификации на 1.

Но опять же, когда вы вызываете collection.remove() непосредственно для коллекции, это увеличивает только значение переменной-номера-модификации для коллекции, но не переменную для итератора.

И правило таково: всякий раз, когда значение номера модификации для итератора не совпадает с исходным значением номера модификации коллекции, оно создает исключение ConcurrentModificationException.

3 голосов
/ 30 августа 2011

Vineet Reynolds подробно объяснил причины, по которым коллекции генерируют ConcurrentModificationException (потокобезопасность, параллелизм). Свагатика подробно объяснил детали реализации этого механизма (как сбор и итератор ведут подсчет количества модификаций).

Их ответы были интересными, и я проголосовал за них. Но в вашем случае проблема не в параллелизме (у вас есть только один поток), и детали реализации, хотя и интересны, не должны здесь рассматриваться.

Вы должны учитывать только эту часть HashSet Javadoc:

Итераторы, возвращаемые методом итератора этого класса, работают быстро: если набор изменяется в любое время после создания итератора, в любым способом, кроме как через собственный метод удаления итератора, итератор генерирует исключение ConcurrentModificationException. Таким образом, перед лицом одновременная модификация, итератор быстро и чисто дает сбой, вместо того, чтобы рисковать произвольным, недетерминированным поведением в неопределенное время в будущем.

В вашем коде вы перебираете свой HashSet, используя его итератор, но вы используете собственный метод удаления HashSet для удаления элементов (C.remove(c)), что вызывает ConcurrentModificationException. Вместо этого, как объяснено в javadoc, вы должны использовать собственный метод remove() Iterator, который удаляет элемент, который в данный момент повторяется, из базовой коллекции.

Заменить

                if(c.isSomething()) C.remove(c);

с

                if(c.isSomething()) it.remove();

Если вы хотите использовать более функциональный подход, вы можете создать Predicate и использовать метод Iterables.removeIf () в Guava для HashSet:

Predicate<BooleanClause> ignoredBooleanClausePredicate = ...;
Multimap<Term, BooleanClause> terms = getTerms(bq);
for (Term term : terms.keySet()) {
    Collection<BooleanClause> booleanClauses = Sets.newHashSet(terms.get(term));
    Iterables.removeIf(booleanClauses, ignoredBooleanClausePredicate);
}

PS: обратите внимание, что в обоих случаях будут удалены только элементы из временного HashSet. Multimap не будет изменено.

...