ConcurrentHashMap

ConcurrentHashMap<K, V> — потокобезопасная map для конкурентного чтения и обновления. В отличие от общей блокировки вокруг HashMap, она позволяет нескольким операциям работать с разными бакетами одновременно.

Устройство Java 8+

В современной реализации нет массива Segment, характерного для Java 7. Основа — массив бакетов. Пустой бакет может устанавливаться через CAS, а обновление занятого бакета координируется на уровне его первого узла. Длинная цепочка может превратиться в дерево по правилам, близким к HashMap.

Чтение get обычно не захватывает monitor. Публикация узлов и их полей построена на volatile/CAS-гарантиях. Resize выполняется совместно: несколько потоков могут переносить разные диапазоны таблицы.

Семантика операций

ConcurrentMap<String, Long> totals = new ConcurrentHashMap<>();
totals.merge("accepted", 1L, Long::sum);
 
String value = totals.computeIfAbsent("key", key -> loadValue(key));

Составная операция get → вычисление → put не атомарна. Для неё существуют putIfAbsent, compute, computeIfAbsent, merge, replace.

Функция compute должна быть короткой и не пытаться рекурсивно изменять те же ключи. Она может блокировать конкурентное обновление соответствующего бакета. Для дорогой загрузки также важно продумать повтор при исключении: неудачный результат в map не записывается.

Ограничения

  • null запрещён и для ключей, и для значений: иначе get == null был бы неоднозначен при конкурентных изменениях.
  • Итераторы weakly consistent: не бросают ConcurrentModificationException, могут увидеть часть параллельных изменений, но не дают snapshot.
  • size() при активных обновлениях пригоден для мониторинга, а не для управляющего условия.
  • Потокобезопасность map не делает потокобезопасным изменяемый объект-значение.
ЗадачаПодход
уникально добавитьputIfAbsent
обновить счётчикmerge или compute
много счётчиков под нагрузкойConcurrentHashMap<K, LongAdder>
получить стабильный снимоквнешняя координация или immutable snapshot

Связанные темы: Atomic типы данных, happens before, Коллекции.

Вопросы для собеседования

  1. Есть ли сегменты в Java 8+? Нет, это устройство старой реализации.
  2. Почему запрещён null? Чтобы отсутствие ключа не путалось с null-значением.
  3. Атомарны ли get и следующий put вместе? Нет; нужен compound-метод.
  4. Блокирует ли get? Обычно нет.
  5. Что видит итератор? Согласованный по отдельным элементам, но не обязательно моментальный снимок.
  6. Безопасно ли менять объект из get? Только если сам объект потокобезопасен или изменение отдельно синхронизировано.
  7. Чем опасен долгий compute? Он задерживает обновления того же бакета.