Перейти к содержимому

Concurrentmodificationexception java что это

  • автор:

Избавляемся от ConcurrentModificationException

Как известно, ConcurrentModificationException к многопоточности никакого отношения не имеет. Возникает эта гадость, когда мы пытаемся модифицировать коллекцию во время итерирования по ней. Как обычно, это имеет исторические корни: коллекции и итераторы появились в Java 1.2, в те времена избежать явного использования итератора при обходе коллекции было никак нельзя, так что предложение менять коллекцию посредством методов итератора не выглядело совсем ужасным:

Iterator iterator = collection.iterator(); while (iterator.hasNext()) < Object element = iterator.next(); if (iDontLikeThisElement(element)) < iterator.remove(); >> 

Не, всё же выглядело. Но никаких других вариантов не было. Позже в пятой джаве появляется цикл foreach, и использование итераторов становится преимущественно неявным:

for (E element : collection) < if (iDonLikeThisElement(element)) < collection.remove(element); // облом! ConcurrentModificationException! >> 

«Ишь чего захотели! Юзайте явные итераторы, дорогие кастомеры, и не выделывайтесь» — наверное что-то такое думали разработчики джава платформы работая над пятеркой.

В шестой джаве появляется пакет конкаренси. Теперь можно cделать так:

Set set = Collections.newSetFromMap(new ConcurrentHashMap<>()); 

И получить set который не кидается ConcurrentModificationException-ами. Но опять же счастье не совсем полное:

  1. Oбычно многопоточность нам вовсе не нужна
  2. Не подерживаются null ни в качестве элементов, ни ключей, ни значений. Да и ладно, честно сказать.
  3. Порядок элементов не определён и может меняться — вот это гораздо хуже. Т.е. если мы бежим по элементам и ведём некий подсчёт с потерей точности, то нас могут поджидать неприятные сюрпризы и разные результаты на одних и тех же наборах данных, что, скажем, не всегда хорошо. Так же бывают задачи, где желательно сохранить именно изначальный порядок данных. Ну и вот такие штуки тоже имеют место быть:
set.add("aaa"); set.add("bbb"); for (String s : set)

aaa
bbb
ddd

set.add("aaa"); set.add("bbb"); for (String s : set)

Поэтому сейчас мы сделаем свою собственную коллекцию с чётким порядком. И так, что мы хотим получить:

  1. В рамках одного треда можно добавлять и удалять элементы в любой момент без всяких эксепшенов. И конечно же за константное время.
  2. Можно хранить null-ы, если вдруг хочется.
  3. Элементы обходятся в том порядке в котором были добавлены.
  1. Удаляя элемент мы не будем обнулять ссылку на следующий, т. е. eсли итератор стоит на данном элементе, то он сможет пройти дальше.
  2. В конце списка поместим фэйковый элемент, который превращается в настоящий когда в список что-нибудь добавляют. Т.е. даже добравшись до конца списка итератор не упирается в null и может продолжить работу если в коллекции появляется новый элемент. Далее в коде этот фейковый элемент называется placeholder.

  1. В начале у нас есть элементы A, B, C, D.
  2. Затем элементы C и D удаляются.
  3. Добавляется новый элемент E.

Ну и для константного времени доступа нам, очевидно, нужен хэшмап:

import java.util.AbstractSet; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; public class LinkedSet extends AbstractSet  < private static class LinkedElement < E value; boolean exists; LinkedElementprev; LinkedElement next; > private Map map = new HashMap<>(); private LinkedElement placeholder = new LinkedElement<>(); private LinkedElement head = placeholder; @Override public boolean isEmpty() < return head == placeholder; >@Override public int size() < return map.size(); >@Override public boolean contains(Object o) < return map.containsKey(o); >// здесь будут методы для добавления, удаления, итерирования > 
 @Override public boolean add(E e) < LinkedElementelement = map.putIfAbsent(e, placeholder); if (element != null) < return false; >element = placeholder; element.exists = true; element.value = e; placeholder = new LinkedElement<>(); placeholder.prev = element; element.next = placeholder; return true; > 
 @Override public boolean remove(Object o) < LinkedElementremovedElement = map.remove(o); if (removedElement == null) < return false; >removeElementFromLinkedList(removedElement); return true; > private void removeElementFromLinkedList(LinkedElement element) < element.exists = false; element.value = null; element.next.prev = element.prev; if (element.prev != null) < element.prev.next = element.next; element.prev = null; >else < head = element.next; >> 
 @Override public Iterator iterator() < return new ElementIterator(); >private class ElementIterator implements Iterator  < LinkedElementnext = head; LinkedElement current = null; LinkedElement findNext() < LinkedElementn = next; while (!n.exists && n.next != null) < next = n = n.next; >return n; > @Override public boolean hasNext() < return findNext().exists; >@Override public E next() < LinkedElementn = findNext(); if (!n.exists) < throw new NoSuchElementException(); >current = n; next = n.next; return n.value; > @Override public void remove() < if (current == null) < throw new IllegalStateException(); >if (map.remove(current.value, current)) < removeElementFromLinkedList(current); >else < throw new NoSuchElementException(); >> > 

Теперь можно делать так:

Set set = new LinkedSet<>(); // . put some numbers set.stream().filter(v -> v % 2 == 0).forEach(set::remove); 

Понятно, что аналогично можно сконструировать и LinkedMap. Вот в общем-то и всё, ещё один велосипед готов. Почему подобным образом не доработали библиотечные LinkedHashMap и LinkedHashSet? Кто знает, возможно чтобы джависты завидовали джаваскриптистам.

  • java
  • коллекции
  • ConcurrentModificationException

Как избежать ConcurrentModificationException при пользовании коллекциями

Название ConcurrentModificationException многих вводит в заблуждение. При слове Concurrent первое что приходит на ум — многотредность. Однако, данное исключение относится вовсе не к многотредности. Исключение может происходить при работе с коллекциями при обычной однотредной работе.

Для «прохода» по коллекции используются структуры данных, реализующие паттерн «View». Структуры данных эти называются итераторами и могут использоваться явно и не явно. Неявно итераторы используются при использовании конструкции foreach.

ConcurrentModificationException возникает когда коллекция модифицируется «одновременно» с проходом по коллекции итератором любыми средствами кроме самого итератора.

Например, ниже при удалении элемента из map произойдет ConcurrentModificationException, поскольку в цикле for неявно формируется итератор, и из map удаляется элемент, в процессе прохода по map.

@Test(expected = ConcurrentModificationException.class) public void testForEachFail() < Mapmap = new HashMap(); map.put("a", "a"); map.put("b", "b"); for(String key:map.keySet()) < map.remove(key); >>

Заблуждения добавляет тот факт, что в случае, если в map в приведенном примере будет лишь один элемент, то ConcurrentModificationException не возникнет (ниже тот же код, но добавляется лишь один элемент в map):

@Test public void testForEachOneElement() < Mapmap = new HashMap(); map.put("a", "a"); for(String key:map.keySet()) < map.remove(key); >>

Тест выше проходит, не смотря на то, что не верен… Еще один пример неверного кода, уже со StackOverflow:

// @see http://stackoverflow.com/questions/602636/concurrentmodificationexception-and-a-hashmap Iterator it = map.entrySet().iterator(); while (it.hasNext())

В вышеприведенном примере, СoncurrentModificationException возникает при явном использовании итератора для прохода по элементам. Но в то же самое время элемент удаляется по ключу: map.remove(item.getKey()).

Как же сделать удаление правильно? Правильно, нужно использовать метод итератора, для удаления элемента:

// @see http://stackoverflow.com/questions/602636/concurrentmodificationexception-and-a-hashmap Iterator it = map.entrySet().iterator(); while (it.hasNext())

Краткое summary

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

Надежность библиотек Java усыпляет. Описанные выше примеры показывают, что в Java все таки есть способы «стрельнуть себе в ногу».

Избегаем ConcurrentModificationException при удалении элементов из ArrayList во время итерации

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

Пример кода, который вызывает это исключение:

ArrayList<String> list = new ArrayList<String>(); list.add("Java"); list.add("PHP"); list.add("Python"); for (String language : list) < if (language.equals("PHP")) < list.remove(language); >>

В этом примере попытка удалить элемент списка во время итерации через него вызывает ConcurrentModificationException.

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

Пример кода, который безопасно удаляет элемент из списка во время итерации:

ArrayList<String> list = new ArrayList<String>(); list.add("Java"); list.add("PHP"); list.add("Python"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) < String language = iterator.next(); if (language.equals("PHP")) < iterator.remove(); >>

Таким образом, для безопасного удаления элементов из коллекции во время итерации по ней следует использовать итератор и его метод remove().

ConcurrentModificationException

Вот ещё что странно из лекции 2 уровень 8. https://javarush.com/quests/lectures/questsyntax.level08.lecture02 — В Java есть сокращённая запись работы с итераторами. По аналогии с while, в for был добавлен еще один специальный оператор «for each» — «для каждого». Обозначается тоже ключевым словом for. — Оператор for each используется только при работе с коллекциями и контейнерами. В нем неявно используется итератор, но мы видим уже полученный элемент. у нас если у for неявно используется итератор — почему нельзя удалять/добавлять элемент? Или это такой мини-итератор только чтобы пройтись по всем элементам коллекции, даже если у них нет номера (Set и Map)

Wladyslaw Java Developer Master
19 июня 2019, 20:36

Знаю что нельзя модифицировать коллекцию напрямую когда по ней итерируешь — только через методы итератора. Логически понятно почему так — курсор в итераторе тогда не понятно на что указывать будет. А как оно «под капотом устроено» — сам пока не разобрался

Уровень 25
16 июня 2019, 09:47

Лекция на эту тему (вторая часть): https://javarush.com/groups/posts/1935-udalenie-ehlementa-iz-spiska-arraylist И годное описание со StackOverFlow: https://ru.stackoverflow.com/questions/454175/%D0%9A%D0%B0%D0%BA-%D0%BE%D0%B1%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B0%D1%82%D1%8C-%D0%B8%D1%81%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D0%B5-java-util-concurrentmodificationexception

Уровень 20
19 июня 2019, 20:30

спасибо, из этих лекций я понял, что всё дело в цикле foreach однако исключение возникает даже в явном использовании итератора(((

  • Курсы программирования
  • Регистрация
  • Курс Java
  • Помощь по задачам
  • Цены
  • Задачи-игры

Сообщество

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *