Какие существуют 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. CopyOnWriteArraySet — Set на базе 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создал бы неоднозначность.