Статьи

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

Классы Java Collection являются отказоустойчивыми, что означает, что если коллекция будет изменена в то время, когда какой-либо поток пересекает ее с помощью итератора, iterator.next () сгенерирует исключение ConcurrentModificationException .

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

Давайте рассмотрим этот сценарий на следующем примере:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.util.*;
  
public class IteratorExample {
  
    public static void main(String args[]){
        List<String> myList = new ArrayList<String>();
  
        myList.add("1");
        myList.add("2");
        myList.add("3");
        myList.add("4");
        myList.add("5");
  
        Iterator<String> it = myList.iterator();
        while(it.hasNext()){
            String value = it.next();
            System.out.println("List Value:"+value);
            if(value.equals("3")) myList.remove(value);
        }
  
        Map<String,String> myMap = new HashMap<String,String>();
        myMap.put("1", "1");
        myMap.put("2", "2");
        myMap.put("3", "3");
  
        Iterator<String> it1 = myMap.keySet().iterator();
        while(it1.hasNext()){
            String key = it1.next();
            System.out.println("Map Value:"+myMap.get(key));
            if(key.equals("2")){
                myMap.put("1","4");
                //myMap.put("4", "4");
            }
        }
  
    }
}

Выход:

1
2
3
4
5
6
7
List Value:1
List Value:2
List Value:3
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
    at java.util.AbstractList$Itr.next(AbstractList.java:343)
    at com.journaldev.java.IteratorExample.main(IteratorExample.java:27)

Из трассировки выходного стека ясно, что исключение наступает, когда мы вызываем функцию iterator next (). Если вам интересно, как Iterator проверяет модификацию, его реализация присутствует в классе AbstractList, где определена переменная int modCount, которая обеспечивает количество изменений размера списка. Это значение используется при каждом вызове next () для проверки любых изменений в функции checkForComodification ().

Теперь прокомментируйте часть списка и снова запустите программу.

Выход будет:

1
2
3
Map Value:3
Map Value:2
Map Value:4

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

Чтобы избежать ConcurrentModificationException в многопоточной среде:

1. Вы можете преобразовать список в массив, а затем выполнить итерацию по массиву. Этот подход хорошо работает для небольшого или среднего размера списка, но если список большой, то это сильно повлияет на производительность.

2. Вы можете заблокировать список во время итерации, поместив его в синхронизированный блок. Этот подход не рекомендуется, потому что он прекратит преимущества многопоточности.

3. Если вы используете JDK1.5 или выше, вы можете использовать классы ConcurrentHashMap и CopyOnWriteArrayList. Это рекомендуемый подход.

Чтобы избежать ConcurrentModificationException в однопоточной среде:

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

Давайте запустим пример с использованием классов Concurrent Collection:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.journaldev.java;
  
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
  
public class ThreadSafeIteratorExample {
  
    public static void main(String[] args) {
  
        List<String> myList = new CopyOnWriteArrayList<String>();
  
        myList.add("1");
        myList.add("2");
        myList.add("3");
        myList.add("4");
        myList.add("5");
  
        Iterator<String> it = myList.iterator();
        while(it.hasNext()){
            String value = it.next();
            System.out.println("List Value:"+value);
            if(value.equals("3")){
                myList.remove("4");
                myList.add("6");
                myList.add("7");
            }
        }
        System.out.println("List Size:"+myList.size());
  
        Map<String,String> myMap =
             new ConcurrentHashMap<String,String>();
        myMap.put("1", "1");
        myMap.put("2", "2");
        myMap.put("3", "3");
  
        Iterator<String> it1 = myMap.keySet().iterator();
        while(it1.hasNext()){
            String key = it1.next();
            System.out.println("Map Value:"+myMap.get(key));
            if(key.equals("1")){
                myMap.remove("3");
                myMap.put("4", "4");
                myMap.put("5", "5");
            }
        }
  
        System.out.println("Map Size:"+myMap.size());
    }
  
}

Выход:

01
02
03
04
05
06
07
08
09
10
11
List Value:1
List Value:2
List Value:3
List Value:4
List Value:5
List Size:6
Map Value:1
Map Value:null
Map Value:4
Map Value:2
Map Size:4

Из приведенного выше примера ясно, что:

1. Классы Concurrent Collection можно изменять, избегая исключения ConcurrentModificationException .

2. В случае CopyOnWriteArrayList итератор не учитывает изменения в списке и работает с исходным списком.

3. В случае ConcurrentHashMap поведение не всегда одинаково.

Для условия:

1
2
if(key.equals("1")){
    myMap.remove("3");

Выход:

1
2
3
4
5
Map Value:1
Map Value:null
Map Value:4
Map Value:2
Map Size:4

Это берет новый объект, добавленный с ключом «4? но не следующий добавленный объект с ключом «5».

Теперь, если я изменю условие на

1
2
if(key.equals("3")){
    myMap.remove("2");

Выход:

1
2
3
4
Map Value:1
Map Value:3
Map Value:null
Map Size:4

В этом случае не учитываются новые добавленные объекты.

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

Дополнительные начинки:

1
2
3
4
5
6
7
8
for(int i = 0; i<myList.size(); i++){
    System.out.println(myList.get(i));
    if(myList.get(i).equals("3")){
        myList.remove(i);
        i--;
        myList.add("6");
    }
}

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

Обратите внимание, что я уменьшаю счетчик, потому что я удаляю тот же объект, если вам нужно удалить следующий или более дальний объект, вам не нужно уменьшать счетчик.

Попробуй сам.

Ссылка: Как избежать ConcurrentModificationException при использовании Итератора от нашего партнера JCG в JournalDev ,