Почему this.next() throw java.util.ConcurrentModificationException?

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, но можете ли вы поднять запах?

Ответ 1

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

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

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

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

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

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

Я считаю, что ссылки на Джавадок объясняют себя тем, что должно быть сделано дальше.

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

Обратите внимание, что хотя a ConcurrentModificationException можно предотвратить с помощью синхронизированного набора (как указано в документации по API Java), это является предварительным условием, что все потоки напрямую обращаются к синхронизированной коллекции, а не к базе данных резервного копирования (которая может быть неверно в вашем случае, поскольку HashSet, вероятно, создается в одном потоке, тогда как базовая коллекция для MultiMap изменяется другими потоками). Синхронизированные классы коллекций фактически поддерживают внутренний мьютекс для потоков, чтобы получить доступ; так как вы не можете получить доступ к мьютексу непосредственно из других потоков (и было бы довольно смешно делать это здесь), вы должны посмотреть на использование защитной копии либо набора ключей, либо самого MultiMap с помощью метода unmodifiableMultimap класса MultiMaps (вам нужно будет вернуть немодифицируемую MultiMap из метода getTerms). Вы также можете исследовать необходимость возврата synchronized 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) для Collection используется KeyIterator (внутренний внутренний класс класса HashMap, который используется HashSet). Особенностью этого Iterator является то, что он отслеживает количество структурных изменений, выполненных в Collection (HashMap в этом случае) с помощью метода Iterator.remove.

Когда вы вызываете remove непосредственно на Collection, а затем следуете за ним вызовом Iterator.next, итератор выдает a 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.
                }
            }
        }

Ответ 2

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

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

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

Поэтому, когда вы вызываете it.remove() через итератор, который увеличивает значение как переменной-number-variable на 1.

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

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

Ответ 3

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

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

Вы должны рассматривать эту часть HashSet javadoc:

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

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

Заменить

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

с

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

Если вы хотите использовать более функциональный подход, вы можете создать Predicate и использовать Guava метод Iterables.removeIf() в 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 не будет изменен.