Модификация коллекции Java в однопоточной и многопоточной среде
Что будет результатом выполнения такой программы?
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
for (String el : list) {
if (el.equals("b")) {
list.remove(el);
}
}
System.out.println(list);
}
}
Эта программа бросит исключение ConcurrentModificationException. Почему так происходит? for-each цикл в Java использует Iterator. И если мы используем итератор, то удаление нужно делать при помощи итератора. Если же мы удаление делаем при помощи метода list.remove(el), то при вызове метода next будет брошено исключение ConcurrentModificationException:
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
Как это исправить?
Можно использовать Iterator явно и вызвать метод remove итератора:
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
String el = (String) iterator.next();
if (el.equals("b")) {
iterator.remove();
}
}
System.out.println(list);
}
}
Результат:
[a, c, d]
Или же можно использовать removeIf:
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.removeIf(el -> el.equals("b"));
System.out.println(list);
}
}
Или не использовать итератор вообще:
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
for (int i = 0; i < list.size(); i++) {
String el = list.get(i);
if (el.equals("b")) {
list.remove(el);
}
}
System.out.println(list);
}
}
Многопоточная среда:
Допустим у нас в одном потоке есть итерирование по коллекции, а во втором потоке у нас происходит модификация этой коллекции (удаление или добавление элемента). Чтобы предотвратить ConcurrentModificationException можно использовать:
- synchronized блок перед итерированием:
- Использовать потокобезопасные коллекции: ConcurrentHashMap, CopyOnWriteArrayList
Посмотрим вариант с synchonized блоком:
Первый поток:
synchronized (list) {
for (String el : list) {
System.out.println(el);
}
}
Второй поток:
synchronized (list) {
list.remove("b");
}
Можно ли избежать synchronized блока, если использовать:
Collections.synchronizedList(list);
Ответ: нет. В таком случае может быть также брошен ConcurrentModificationException. В таком случае также нужно помещать в synchonized блок.
Если мы используем ConcurrentHashMap, CopyOnWriteArrayList то можно итерироваться по коллекции в одном потоке и модифицировать ее в другом без synchonized блока не опасаясь ConcurrentModificationException.