Как в Java поступают с одновременными изменениями коллекции?

В Java одновременные (конкурентные) изменения коллекций — это важная тема, особенно в контексте многопоточных программ. Коллекции из стандартного пакета java.util не являются потокобезопасными по умолчанию, и попытка изменять их одновременно из нескольких потоков может привести к состоянию гонки, искажению данных или исключениям времени выполнения (ConcurrentModificationException). Чтобы корректно обрабатывать конкурентный доступ, в Java существуют специальные подходы, API и структуры данных.

🔹 Проблемы при одновременном доступе

1. Race condition (состояние гонки)

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

2. ConcurrentModificationException

Вызывается, когда один поток итерирует коллекцию, а другой модифицирует её:

List&lt;String&gt; list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
list.remove(s); // java.util.ConcurrentModificationException
}

🔧 Способы защиты от одновременных изменений

1. Collections.synchronizedXXX()

Java предоставляет обёртки, которые синхронизируют доступ к коллекциям:

List&lt;String&gt; 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&lt;String&gt; 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&lt;String, Integer&gt; 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&lt;String, AtomicInteger&gt; wordCounts = new ConcurrentHashMap<>();
wordCounts.computeIfAbsent("word", k -> new AtomicInteger()).incrementAndGet();

5. Синхронизация вручную (synchronized, ReentrantLock)

Вы можете использовать synchronized или Lock для управления доступом к обычным коллекциям:

List&lt;String&gt; 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&lt;String&gt; list = new ArrayList<>(List.of("a", "b", "c"));
Iterator&lt;String&gt; 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 (если не синхронизировано явно)
--- ---

📌 Подходы к обеспечению безопасности коллекций

  1. Выбор нужной структуры:

    • Частые чтения → CopyOnWriteArrayList

    • Частые изменения → ConcurrentHashMap

  2. Разделение доступа по задачам:

    • Один поток пишет, другие читают → может хватить volatile или CopyOnWrite.
  3. Использование блокировок:

    • ReentrantLock, ReadWriteLock — полезны, если чтения преобладают над записями.
  4. Иммутабельность:

    • List.copyOf(...), Collections.unmodifiableList(...) — возвращают неизменяемую обёртку.

📦 Примеры практического использования

Очередь задач в многопоточном окружении:

BlockingQueue&lt;Runnable&gt; taskQueue = new LinkedBlockingQueue<>();
// Производитель
taskQueue.put(() -> doWork());
// Потребитель
Runnable task = taskQueue.take();
task.run();
Обновление счётчиков с гарантией атомарности:
ConcurrentHashMap&lt;String, AtomicLong&gt; 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 предоставляет разнообразные инструменты и подходы для безопасной работы с коллекциями в многопоточном окружении — от примитивной синхронизации до специализированных структур, адаптированных под различные сценарии конкурентного доступа.