Gymterview
middle

Какие существуют Concurrent Collections и в чём их особенности?

Пакет java.util.concurrent содержит потокобезопасные коллекции, спроектированные для эффективной работы в многопоточной среде без внешней синхронизации.

1. ConcurrentHashMap — основная рабочая лошадка

Потокобезопасная хэш-таблица, заменяющая Hashtable и Collections.synchronizedMap().

  • Не блокирует всю таблицу — в Java 8+ использует блокировку на уровне отдельных бакетов + CAS.
  • Операции чтения обычно без блокировки.
  • Не допускает null ни как ключ, ни как значение.
  • Итератор weakly consistent.
Пример
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);

// Атомарные составные операции
map.putIfAbsent("key2", 2);
map.compute("key1", (key, value) -> value + 1);
map.merge("key1", 10, Integer::sum);

// Параллельные агрегатные операции (Java 8+)
long count = map.reduceValues(1, Long::sum);
map.forEach(2, (key, value) -> System.out.println(key + "=" + value));

2. CopyOnWriteArrayList — для редко изменяемых списков

Каждая мутация создаёт новую копию внутреннего массива.

  • Чтение — без блокировки, без затрат.
  • Запись — дорогая (копирование всего массива).
  • Идеально для списков слушателей/observers.
  • Итератор — snapshot, не выбрасывает ConcurrentModificationException.

3. CopyOnWriteArraySetSet на базе CopyOnWriteArrayList.

4. ConcurrentLinkedQueue — lock-free очередь

Неблокирующая очередь на алгоритме Майкла-Скотта.

  • Использует CAS-операции.
  • size() — O(n), дорогая операция.
  • Не ограничена по размеру.

5. ConcurrentLinkedDeque — lock-free двусторонняя очередь.

6. BlockingQueue (интерфейс) и реализации:

Реализация Особенность
ArrayBlockingQueue Ограниченная, на массиве
LinkedBlockingQueue Опционально ограниченная, на связном списке
PriorityBlockingQueue Неограниченная, приоритетная
DelayQueue Элементы доступны после истечения задержки
SynchronousQueue Ёмкость 0: put() ждёт take()
LinkedTransferQueue transfer() ждёт, пока элемент будет потреблён
Пример
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
queue.put("данные");       // блокирует, если заполнена
String data = queue.take(); // блокирует, если пуста

7. ConcurrentSkipListMap и ConcurrentSkipListSet — потокобезопасные отсортированные коллекции (аналоги TreeMap/TreeSet). Все операции O(log n).

Сравнение подходов к потокобезопасности:

Подход Механизм Пример
synchronized Единый мьютекс Collections.synchronizedMap()
Сегментная блокировка Блокировка части данных ConcurrentHashMap (до Java 8)
CAS + lock per bucket Атомарные операции ConcurrentHashMap (Java 8+)
Copy-on-write Копия при записи CopyOnWriteArrayList
Lock-free Только CAS ConcurrentLinkedQueue

Частые ошибки:

  • CopyOnWriteArrayList для часто изменяемых коллекций — каждая модификация копирует массив.
  • if (!map.containsKey(key)) map.put(key, value)не атомарно! Используйте putIfAbsent().
  • size() на ConcurrentLinkedQueue в цикле — O(n) на каждый вызов.
  • null в ConcurrentHashMap — запрещено, бросит NullPointerException.

Выбор коллекции:

Сценарий Коллекция
Потокобезопасная Map ConcurrentHashMap
Отсортированная Map ConcurrentSkipListMap
Много чтений, мало записей (список) CopyOnWriteArrayList
Producer-consumer (блокирующий) ArrayBlockingQueue / LinkedBlockingQueue
Producer-consumer (неблокирующий) ConcurrentLinkedQueue

На собеседовании частый вопрос: «Почему ConcurrentHashMap не допускает null?» Ответ: в отличие от HashMap, где null-значение можно отличить от отсутствия ключа через containsKey(), в конкурентной среде между containsKey() и get() состояние может измениться, поэтому null создал бы неоднозначность.