Получить доступ

Конкурентные коллекции

ConcurrentHashMap без сегментов (CAS + блокировка бакета), запрет null, CopyOnWriteArrayList и BlockingQueue.

Зачем отдельное семейство

Обычные коллекции не потокобезопасны. Обёртки Collections.synchronizedMap/List берут единый замок на всю коллекцию — это корректно, но превращает любой доступ в узкое место и всё равно требует ручной синхронизации при итерации.

Конкурентные коллекции из java.util.concurrent спроектированы под параллелизм: тонкая блокировка или вовсе lock-free чтение, плюс weakly-consistent итераторы без CME.

ConcurrentHashMap: устройство

Современный ConcurrentHashMap (с Java 8) отказался от старых сегментов. Теперь это та же таблица бакетов, что у HashMap, но с потокобезопасным доступом по бакетам: вставка в пустой бакет делается атомарно через CAS, а при коллизии берётся synchronized на головном узле конкретного бакета.

Так конкурентные операции над разными бакетами не мешают друг другу, и масштабируемость на запись близка к числу бакетов. Treeify и resize работают, причём ресайз кооперативный — потоки помогают переносить бакеты.

Почему запрещены null

ConcurrentHashMap не допускает null ни как ключ, ни как значение. Причина — неоднозначность в конкурентной среде: map.get(k) == null нельзя трактовать ни как «ключа нет», ни как «значение null», и без блокировки это не различить (containsKey мог бы устареть к следующей строке).

Обычный HashMap такую неоднозначность разрешает в однопоточном коде, поэтому там null допустим.

Атомарные операции

Главная сила ConcurrentHashMap — атомарные составные операции: computeIfAbsent, merge, compute выполняются атомарно на уровне бакета, заменяя небезопасную связку «проверил-потом-положил» (check-then-act).

Это правильный способ строить, например, конкурентные счётчики или кэши без внешних замков.

// атомарный конкурентный счётчик
counts.merge(key, 1L, Long::sum);
// атомарная ленивая инициализация значения
cache.computeIfAbsent(key, this::loadExpensive);

CopyOnWriteArrayList и BlockingQueue

CopyOnWriteArrayList на каждую запись копирует весь внутренний массив, зато чтения идут без блокировок по неизменяемому снимку. Это идеально для read-mostly данных с редкой записью (списки слушателей/подписчиков) и плохо — для частых изменений.

BlockingQueue (ArrayBlockingQueue — ограниченная на массиве, LinkedBlockingQueue — опционально ограниченная) — основа паттерна producer-consumer: put блокируется на заполненной очереди, take — на пустой, что даёт встроенный backpressure.

Как отвечать на собеседовании

Эталон: ConcurrentHashMap = бакеты HashMap + CAS на пустой бакет и synchronized на голову бакета (а не один глобальный замок), отсюда масштабируемость; null запрещён из-за неоднозначности get; составные операции (computeIfAbsent/merge) атомарны. Добавьте CopyOnWriteArrayList для read-mostly и BlockingQueue для producer-consumer.

Вопросы по теме

Читайте также

Закрытая бета

Знать тему — половина дела

На живом собеседовании sotto слышит вопрос интервьюера и показывает короткий ответ и план. Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем