Чому викидається ConcurrentModificationException і як його налагодити


130

Я використовую Collection( HashMapвикористовуваний опосередковано JPA, це трапляється так), але, мабуть, випадковим чином код закидає а ConcurrentModificationException. Що це викликає і як виправити цю проблему? Можливо, використовуючи деяку синхронізацію?

Ось повний слід стека:

Exception in thread "pool-1-thread-1" java.util.ConcurrentModificationException
        at java.util.HashMap$HashIterator.nextEntry(Unknown Source)
        at java.util.HashMap$ValueIterator.next(Unknown Source)
        at org.hibernate.collection.AbstractPersistentCollection$IteratorProxy.next(AbstractPersistentCollection.java:555)
        at org.hibernate.engine.Cascade.cascadeCollectionElements(Cascade.java:296)
        at org.hibernate.engine.Cascade.cascadeCollection(Cascade.java:242)
        at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:219)
        at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:169)
        at org.hibernate.engine.Cascade.cascade(Cascade.java:130)

1
Чи можете ви надати ще якийсь контекст? Ви об'єднуєте, оновлюєте чи видаляєте об'єкт? Які асоціації мають ця організація? А що з каскадними налаштуваннями?
ordnungswidrig

1
З сліду стека видно, що виняток відбувається під час ітерації через HashMap. Безумовно, якийсь інший потік модифікує карту, але виняток трапляється в потоці, який повторюється.
Чочос

Відповіді:


263

Це не проблема синхронізації. Це відбудеться, якщо основну колекцію, яку повторюють, буде модифіковано чим-небудь, крім самого Ітератора.

Iterator it = map.entrySet().iterator();
while (it.hasNext())
{
   Entry item = it.next();
   map.remove(item.getKey());
}

Це буде кидати a, ConcurrentModificationExceptionколи it.hasNext()викликається другий раз.

Правильний підхід був би

   Iterator it = map.entrySet().iterator();
   while (it.hasNext())
   {
      Entry item = it.next();
      it.remove();
   }

Якщо ітератор підтримує remove()операцію.


1
Можливо, але виглядає так, ніби сплячий режим робить ітерацію, яку слід реалізувати досить правильно. Можливо, зворотний виклик може змінити карту, але це малоймовірно. Непередбачуваність вказує на актуальну проблему одночасності.
Том Хотін - тайклін

Цей виняток не має нічого спільного з одночасністю нарізання різьби, він викликаний зміною резервного сховища ітератора. Будь-який інший потік не має значення для ітератора. ІМХО - це виняток з поганою назвою, оскільки він створює неправильне враження про причину.
Робін

Однак я погоджуюся, що якщо це непередбачувано, найімовірніше, виникає проблема з ниткою, яка спричиняє умови для цього винятку. Що робить його ще більш заплутаним через назву винятку.
Робін

Це правильне і краще пояснення, ніж прийнята відповідь, але прийнята відповідь - це приємне виправлення. ConcurrentHashMap не підлягає CME, навіть усередині ітератора (хоча ітератор все ще розроблений для доступу в один потік).
G__

У цьому рішенні немає сенсу, оскільки в Картах немає методу ітератора (). Приклад Робіна може бути застосовний, наприклад, до списків.
пітер

72

Спробуйте використовувати ConcurrentHashMapзамість звичайноїHashMap


Це справді вирішило проблему? У мене виникає та сама проблема, але я, безумовно, можу виключити будь-які проблеми з ниткою.
tobiasbayer

5
Ще одне рішення - створити копію карти та замість неї повторити її. Або скопіюйте набір ключів і повторіть їх, отримуючи значення для кожного ключа з оригінальної карти.
Чохос

Це сплячий режим ітералізує колекцію, тому ви не можете просто скопіювати її.
tobiasbayer

1
Миттєвий рятівник. Збираюся розібратися, чому це спрацювало так добре, щоб я не отримував більше сюрпризів далі вниз.
Вальхріс

1
Я думаю, що це не проблема синхронізації, це проблема, якщо модифікація тієї самої модифікації під час циклу того ж об'єкта.
Rais Alam

17

Модифікація в Collectionтой час як перебір , що з Collectionвикористанням Iteratorце не дозволяються більшістю Collectionкласів. Бібліотека Java називає спробу змінити Collectionітерацію через неї "одночасною модифікацією". Це, на жаль, говорить про єдину можливу причину - одночасну модифікацію кількома потоками, але це не так. Використовуючи лише один потік, можна створити ітератор для Collection(використовуючи Collection.iterator()або посилений forцикл ), запустити ітерацію (використовуючи Iterator.next()або еквівалентно входячи в тіло розширеного forциклу), змінити Collection, а потім продовжити ітерацію.

Щоб допомогти програмістам, деякі реалізації цих Collectionкласів намагаються виявити помилкові одночасні модифікації та кинути а, ConcurrentModificationExceptionякщо вони виявлять його. Однак загалом неможливо та практично гарантувати виявлення всіх одночасних модифікацій. Таким чином, помилкове використання Collectionне завжди призводить до закидання ConcurrentModificationException.

Документація ConcurrentModificationExceptionговорить:

Цей виняток може бути викинуто методами, які виявили одночасну модифікацію об'єкта, коли така модифікація не допустима ...

Зауважте, що цей виняток не завжди вказує на те, що об'єкт одночасно був змінений іншим потоком. Якщо один потік видає послідовність викликів методу, що порушує контракт об'єкта, об'єкт може викинути цей виняток ...

Зауважте, що невдала поведінка не може бути гарантована, оскільки, взагалі кажучи, неможливо дати будь-які жорсткі гарантії за наявності несинхронізованих одночасних модифікацій. Невдалі операції кидаються ConcurrentModificationExceptionна основі найкращих зусиль.

Зауважте, що

Документація з HashSet, HashMap, TreeSetі ArrayListкласів каже , що це:

Ітератори, що повертаються [безпосередньо чи опосередковано з цього класу], є невдалими: якщо [колекція] модифікується в будь-який час після створення ітератора, будь-яким способом, крім методу власного видалення ітератора, Iteratorвикидання a ConcurrentModificationException. Таким чином, в умовах одночасних модифікацій ітератор виходить з ладу швидко і чисто, а не ризикує довільною недетермінованою поведінкою у невизначений час у майбутньому.

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

Зауважте ще раз, що поведінку "не можна гарантувати" та є лише "на основі найкращих зусиль".

Документація декількох методів Mapінтерфейсу говорить про це:

Несупутнє впровадження повинно змінити цей метод і, на основі найкращих зусиль, кинути а, ConcurrentModificationExceptionякщо виявлено, що функція відображення модифікує цю карту під час обчислення. Одночасні реалізації повинні замінити цей метод і, на основі найкращих зусиль, кинути, IllegalStateExceptionякщо виявлено, що функція відображення модифікує цю карту під час обчислень, і в результаті обчислення ніколи не завершиться.

Зауважте ще раз, що для виявлення потрібна лише "основа найкращих зусиль", а ConcurrentModificationExceptionявно пропонується лише для класів, які не є одночасними (не захищені від потоку).

Налагодження ConcurrentModificationException

Отже, побачивши трасування стека через a ConcurrentModificationException, ви не можете відразу припустити, що причиною є небезпечний багатопотоковий доступ до a Collection. Ви повинні вивчити трасування стека, щоб визначити, який клас Collectionвикинув виняток (метод класу буде прямо чи опосередковано кинутий його) та для якого Collectionоб'єкта. Потім ви повинні вивчити, звідки цей об’єкт можна змінити.

  • Найчастішою причиною є модифікація в Collectionмежах посиленого forциклу над Collection. Тільки те, що ви не бачите Iteratorоб’єкт у своєму вихідному коді, не означає, що його немає Iterator! На щастя, одне з висловлювань несправного forциклу, як правило, знаходиться у сліді стека, тому відстежувати помилку зазвичай легко.
  • Більш складний випадок - коли ваш код обходить посилання на Collectionоб'єкт. Зауважте, що немодифіковані представлення колекцій (таких як створені компанією Collections.unmodifiableList()) зберігають посилання на колекцію, що може змінюватися, тому ітерація над "немодифікованою" колекцією може викинути виняток (модифікація зроблена в іншому місці). Інші представлення ваших підрозділівCollection , такі як підсписки , Mapнабори записів та Mapнабори ключів, також зберігають посилання на оригінал (змінюються) Collection. Це може бути проблемою навіть для безпечного потоку Collection, наприклад CopyOnWriteList; не вважайте, що безпечні для потоків (одночасні) колекції ніколи не можуть кинути виняток.
  • Які операції можуть модифікувати а, Collectionможуть бути несподіваними в деяких випадках. Наприклад, LinkedHashMap.get()модифікує свою колекцію .
  • Найважчі випадки, коли виняток відбувається через одночасну модифікацію декількома потоками.

Програмування для запобігання одночасних помилок модифікації

Коли це можливо, обмежте всі посилання на Collectionоб'єкт, тому його легше запобігти одночасним модифікаціям. Зробіть Collectionна privateоб'єкт або локальну змінну, а не повертати посилання на Collectionабо його ітератори з методів. Тоді набагато простіше оглянути всі місця, де Collectionможна змінити. Якщо використовується Collectionдля використання декількох потоків, то практичним є забезпечення того, щоб потоки Collectionмали доступ до єдиного з відповідною синхронізацією та блокуванням.


Цікаво, чому паралельна модифікація не допускається у випадку одного потоку. Які проблеми можуть виникнути, якщо одному потоку дозволено одночасно змінювати звичайну хеш-карту?
MasterJoe

4

У Java 8 ви можете використовувати лямбда-вираз:

map.keySet().removeIf(key -> key condition);

2

Це менш схоже на проблему синхронізації Java і більше нагадує проблему блокування бази даних.

Я не знаю, якщо додавання версії до всіх ваших стійких класів вдасться розібратися, але це один із способів того, як Hibernate може забезпечити ексклюзивний доступ до рядків у таблиці.

Можливо, рівень ізоляції повинен бути вищим. Якщо ви дозволите "брудні читання", можливо, вам доведеться натрапити на серіалізацію.


Я думаю, що вони мали на увазі Хештел. Він поставляється як частина JDK 1.0. Як і Vector, це було написано як безпечна нитка - і повільна. І те й інше замінено безпечними альтернативами, що не стосуються потоків: HashMap та ArrayList. Платіть за те, що використовуєте.
duffymo

0

Спробуйте або CopyOnWriteArrayList, або CopyOnWriteArraySet, залежно від того, що ви намагаєтеся зробити.


0

Зауважте, що обраний відповідь не може бути застосований до вашого контексту безпосередньо перед деякою модифікацією, якщо ви намагаєтесь видалити деякі записи з карти під час ітерації карти так само, як я.

Я просто даю тут свій робочий приклад для новачків, щоб заощадити свій час:

HashMap<Character,Integer> map=new HashMap();
//adding some entries to the map
...
int threshold;
//initialize the threshold
...
Iterator it=map.entrySet().iterator();
while(it.hasNext()){
    Map.Entry<Character,Integer> item=(Map.Entry<Character,Integer>)it.next();
    //it.remove() will delete the item from the map
    if((Integer)item.getValue()<threshold){
        it.remove();
    }

0

Я зіткнувся з цим винятком, коли намагався видалити x останні елементи зі списку. myList.subList(lastIndex, myList.size()).clear();було єдиним рішенням, яке працювало на мене.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.