При работе над более крупными проектами и оптимизации производительности и масштабирования нам неизбежно приходится иметь дело с потоками. Потоки допускают параллельное выполнение. В 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 г.