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, Коллекции.
Вопросы для собеседования
- Есть ли сегменты в Java 8+? Нет, это устройство старой реализации.
- Почему запрещён null? Чтобы отсутствие ключа не путалось с null-значением.
- Атомарны ли
getи следующийputвместе? Нет; нужен compound-метод. - Блокирует ли
get? Обычно нет. - Что видит итератор? Согласованный по отдельным элементам, но не обязательно моментальный снимок.
- Безопасно ли менять объект из
get? Только если сам объект потокобезопасен или изменение отдельно синхронизировано. - Чем опасен долгий
compute? Он задерживает обновления того же бакета.