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

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

Добро пожаловать в ConcurrentModificationException

Давайте начнем с надуманного примера добавления элементов в ArrayList через три потока.

Когда он запускается, мы начинаем печатать такие вещи, как это

Да, порядок печати тоже может быть перемешан, потому что они имеют 3 потока.

Но вскоре мы наткнулись на эту загвоздку

Exception in thread "t3" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList$Itr.next(ArrayList.java:851) at java.util.AbstractCollection.toString(AbstractCollection.java:461) at in.championswimmer.Main.lambda$main$0(Main.java:16)

Это происходит потому, что класс Java ArrayList имеет переменные modCount и expectedModCount, которые он использует для отслеживания итератора и размера списка. Что, если не равно, приводит к этому исключению.
В конце концов, 2 потока потерпят неудачу, а оставшийся будет продолжаться до конца.

Возможные решения

Можем ли мы synchronized это?

Хорошо, одно наивное решение, которое люди пробуют, - это заключить проблемный код в блок synchronized.
Если мы заключим только операцию ArrayList#add(), это не решит нашу проблему.

synchronized (numList) { 
  numList.add(r.nextInt(50)); 
}

Просто оберните строку add в блок synchronized и запустите и проверьте, что происходит.

Хорошо, а как насчет synchronized всего цикла while?

synchronized (numList) { 
  while (numList.size() < 100) { 
    numList.add(r.nextInt(50)); 
    System.out.print("Thread = " + Thread.currentThread().getName());    
    System.out.println(numList.toString()); 
  } 
}

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

CopyOnWriteArrayList

Другое решение - CopyOnWriteArrayList, специальный класс в Java, который делает копию всего массива при каждой операции записи и, таким образом, делает операции записи в List потокобезопасным.

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

Использование Collections.synchronizedList()

Еще одно решение - использовать Collections.synchronizedList(), который создает синхронизированный и поток List.

Помните, что при выполнении list.iterator().next() мы должны вручную поместить это в синхронизированный блок, если мы когда-либо будем использовать итератор SynchronizedList.

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

Первоначально опубликовано на blog.codingblocks.com 13 марта 2019 г.