Как в Java поступают с одновременными изменениями коллекции?
В Java одновременные (конкурентные) изменения коллекций — это важная тема, особенно в контексте многопоточных программ. Коллекции из стандартного пакета java.util не являются потокобезопасными по умолчанию, и попытка изменять их одновременно из нескольких потоков может привести к состоянию гонки, искажению данных или исключениям времени выполнения (ConcurrentModificationException). Чтобы корректно обрабатывать конкурентный доступ, в Java существуют специальные подходы, API и структуры данных.
🔹 Проблемы при одновременном доступе
1. Race condition (состояние гонки)
Когда несколько потоков одновременно читают и записывают в коллекцию, результат может быть непредсказуемым, т.к. операции не атомарны.
2. ConcurrentModificationException
Вызывается, когда один поток итерирует коллекцию, а другой модифицирует её:
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
list.remove(s); // java.util.ConcurrentModificationException
}
🔧 Способы защиты от одновременных изменений
1. Collections.synchronizedXXX()
Java предоставляет обёртки, которые синхронизируют доступ к коллекциям:
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
- Оборачивает методы в synchronized блоки.
Подходит для простых сценариев, но не гарантирует синхронный доступ при итерации:
<br/>synchronized(syncList) {
for (String s : syncList) {
// безопасный доступ
}
}
- Пример: Collections.synchronizedMap(), Collections.synchronizedSet(), Collections.synchronizedList()
2. CopyOnWrite коллекции (java.util.concurrent)
Коллекции семейства CopyOnWrite создают копию всей структуры при каждом изменении. Они потокобезопасны без необходимости внешней синхронизации.
Примеры:
- CopyOnWriteArrayList
- CopyOnWriteArraySet
Пример:
List<String> list = new CopyOnWriteArrayList<>();
list.add("a");
for (String s : list) {
list.remove(s); // работает без ConcurrentModificationException
}
Особенности:
-
Очень эффективны, если:
-
Много чтений
-
Мало записей
-
-
Медленные при частом изменении, потому что создаётся новая копия массива.
3. Concurrent коллекции (java.util.concurrent)
Java предоставляет специально разработанные коллекции для многопоточного доступа:
Коллекция | Особенности |
---|---|
ConcurrentHashMap | Потокобезопасный, не блокирует весь map, а только сегменты |
--- | --- |
ConcurrentSkipListMap | Отсортированный map (аналог TreeMap) с конкурентной вставкой |
--- | --- |
ConcurrentLinkedQueue | Очередь без блокировок |
--- | --- |
ConcurrentLinkedDeque | Двусторонняя очередь без блокировок |
--- | --- |
LinkedBlockingQueue | Очередь с блокировками, используется в пулах потоков |
--- | --- |
ConcurrentSkipListSet | Потокобезопасный аналог TreeSet |
--- | --- |
BlockingQueue, BlockingDeque | Интерфейсы с методами put, take — блокируют потоки при пустой/полной очереди |
--- | --- |
Пример ConcurrentHashMap:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
map.compute("key", (k, v) -> v + 1); // атомарное обновление
Особенности:
-
Использует локи на сегменты, начиная с Java 8 — lock-free на уровне bin’ов.
-
Позволяет высокую параллельность, не блокируя полностью map.
4. Использование атомарных операций
Комбинация ConcurrentHashMap и атомарных объектов (AtomicInteger, AtomicLong, AtomicReference) позволяет эффективно обновлять значения без гонок.
ConcurrentHashMap<String, AtomicInteger> wordCounts = new ConcurrentHashMap<>();
wordCounts.computeIfAbsent("word", k -> new AtomicInteger()).incrementAndGet();
5. Синхронизация вручную (synchronized, ReentrantLock)
Вы можете использовать synchronized или Lock для управления доступом к обычным коллекциям:
List<String> list = new ArrayList<>();
Object lock = new Object();
void safeAdd(String s) {
synchronized(lock) {
list.add(s);
}
}
Или с ReentrantLock:
ReentrantLock lock = new ReentrantLock();
void safeAdd(String s) {
lock.lock();
try {
list.add(s);
} finally {
lock.unlock();
}
}
Подходит для более точного контроля, особенно если требуется try-lock, ожидание или прерывание.
🛠 Механизмы защиты в стандартных коллекциях
Некоторые коллекции выбрасывают ConcurrentModificationException, отслеживая изменения через модификатор модификации (modCount). Например, ArrayList, HashSet, HashMap.
Пример:
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
Iterator<String> it = list.iterator();
while (it.hasNext()) {
list.add("d"); // модCount увеличился вне итератора — исключение
it.next();
}
Чтобы избежать — используйте Iterator.remove() вместо Collection.remove() или concurrent коллекции.
📂 Итерация в конкурентных коллекциях
Коллекция | Итератор |
---|---|
ArrayList | Fail-fast |
--- | --- |
CopyOnWriteArrayList | Не выбрасывает исключений |
--- | --- |
ConcurrentHashMap | Weakly consistent (может видеть новые данные, но не гарантирует) |
--- | --- |
HashMap | Fail-fast |
--- | --- |
Collections.synchronizedList | Fail-fast (если не синхронизировано явно) |
--- | --- |
📌 Подходы к обеспечению безопасности коллекций
-
Выбор нужной структуры:
-
Частые чтения → CopyOnWriteArrayList
-
Частые изменения → ConcurrentHashMap
-
-
Разделение доступа по задачам:
- Один поток пишет, другие читают → может хватить volatile или CopyOnWrite.
-
Использование блокировок:
- ReentrantLock, ReadWriteLock — полезны, если чтения преобладают над записями.
-
Иммутабельность:
- List.copyOf(...), Collections.unmodifiableList(...) — возвращают неизменяемую обёртку.
📦 Примеры практического использования
Очередь задач в многопоточном окружении:
BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>();
// Производитель
taskQueue.put(() -> doWork());
// Потребитель
Runnable task = taskQueue.take();
task.run();
Обновление счётчиков с гарантией атомарности:
ConcurrentHashMap<String, AtomicLong> counters = new ConcurrentHashMap<>();
counters.computeIfAbsent("event", k -> new AtomicLong()).incrementAndGet();
📌 Java и параллельные коллекции: эволюция
Версия Java | Добавления |
---|---|
Java 1.2 | Коллекции, Collections.synchronizedXXX |
--- | --- |
Java 1.5 | java.util.concurrent, ConcurrentHashMap, CopyOnWriteArrayList, и др. |
--- | --- |
Java 8 | computeIfAbsent, параллельные стримы (parallelStream) |
--- | --- |
Java 9+ | List.of(...), Map.of(...), Set.of(...) — immutable коллекции |
--- | --- |
Таким образом, Java предоставляет разнообразные инструменты и подходы для безопасной работы с коллекциями в многопоточном окружении — от примитивной синхронизации до специализированных структур, адаптированных под различные сценарии конкурентного доступа.