Что такое ConcurrentHashMap?
В отличие от Collections.synchronizedMap(new HashMap<>()) (одна блокировка на всю мапу), CHM позволяет множеству потоков работать параллельно, блокируя только отдельные корзины.
🟢 Junior Level
ConcurrentHashMap — потокобезопасная Map для многопоточных приложений.
В отличие от Collections.synchronizedMap(new HashMap<>()) (одна блокировка на всю мапу), CHM позволяет множеству потоков работать параллельно, блокируя только отдельные корзины.
Главное отличие от HashMap:
- HashMap: не безопасна для потоков
- ConcurrentHashMap: безопасна, без блокировок для чтения
Map<String, Integer> map = new ConcurrentHashMap<>();
// Безопасно из разных потоков
map.put("A", 1);
map.get("A"); // → 1
// null нельзя!
map.put("A", null); // → NullPointerException!
Правило:
- Один поток или thread-local → HashMap
- Несколько потоков читают и пишут → ConcurrentHashMap
- Несколько потоков только читают → подойдёт и HashMap (после safe publication)
🟡 Middle Level
Эволюция
Java 7: Segment Locking
Java 7 — историческая справка, не используется в современном коде.
Мапа разделена на 16 сегментов
→ Потоки на разных сегментах не блокируются
→ size() = блокировка ВСЕХ сегментов!
Java 8+: Node-level Locking
CAS для пустых корзин (без блокировок!)
Synchronized только для коллизий (на корзину!)
→ Максимальный параллелизм
Атомарные операции
map.putIfAbsent(key, value); // Вставить если нет
map.computeIfAbsent(key, k -> new ArrayList<>())
.add(value); // Вычислить и вставить
map.merge(key, 1, Integer::sum); // Атомарное обновление
Особенности
// ❌ null запрещён!
map.put(key, null); // → NullPointerException
// ⚠️ size() приблизительный!
int size = map.size(); // Может измениться сразу!
// ✅ mappingCount() для больших коллекций
long count = map.mappingCount(); // > 2^31
// ✅ Итераторы НЕ бросают Exception
// → Weakly consistent
// → Weakly consistent = итератор видит данные на момент создания и может (но не обязан) увидеть последующие изменения. Никогда не бросает ConcurrentModificationException.
for (var entry : map) { ... } // Безопасно
🔴 Senior Level
Lock-free Reads
// get() БЕЗ блокировок!
// Через volatile поля:
// table array → volatile
// Node.val → volatile
// Node.next → volatile
// Happens-Before гарантия:
// → Читатель видит последнюю запись
CAS + Synchronized
CAS (Compare-And-Swap) — атомарная процессорная инструкция: «запиши новое значение, только если текущее равно ожидаемому». Работает без блокировок — если два потока одновременно пишут, один succeeds, другой retry.
put(key, value):
1. Корзина пуста → CAS (без блокировок!)
2. Корзина занята → synchronized на HEAD
→ Только эта корзина заблокирована
→ Остальные работают параллельно
Parallel Resizing
При ресайзе:
→ ForwardingNode в старых корзинах
→ Потоки-писатели ВИДЯТ ForwardingNode
→ Вызывают helpTransfer() → ПОМОГАЮТ ресайзить!
→ Распределённый ресайз, без полной блокировки
CounterCells
Вместо одной volatile size:
→ Массив CounterCell (как LongAdder)
→ Каждый поток инкрементирует СВОЮ ячейку
→ size() = сумма всех ячеек
→ Минимальный contention!
Compute ловушка
// ❌ Долгая операция в compute
map.computeIfAbsent(key, k -> {
Thread.sleep(10000); // Блокирует корзину на 10 сек!
return value;
});
// ❌ Рекурсивный доступ → Deadlock!
map.computeIfAbsent(key1, k -> {
return map.get(key2); // Может ждать ту же корзину!
});
Java 21: Virtual Threads
synchronized в CHM может "пинить" виртуальные потоки
→ Но критические секции ОЧЕНЬ короткие
→ Редко проблема на практике
Если проблема:
→ async-profiler покажет blocked время
Production Experience
Реальный сценарий: Bad hashCode
// Все ключи → одна корзина
// → Synchronized на одну корзину
// → 100 потоков ждут → 0 параллелизма!
// Решение: переписать hashCode()
// Результат: +500% throughput
Best Practices
- По умолчанию для многопоточности
- null запрещён — защита от неоднозначности
- computeIfAbsent — атомарная инициализация
- Не долгие операции в compute
- mappingCount() > size() для > 2^31
- Bad hashCode → главный враг параллелизма
- Weakly consistent итераторы
Резюме для Senior
- CAS для пустых корзин, synchronized для коллизий
- Lock-free чтение через volatile
- Parallel resizing с helpTransfer()
- CounterCells = масштабируемый счётчик
- compute → не долгие операции, нет рекурсии
- null запрещён — философское решение
- Bad hashCode → contention на одну корзину
🎯 Шпаргалка для интервью
Обязательно знать:
- ConcurrentHashMap — thread-safe Map, в отличие от HashMap позволяет параллельную работу без полной блокировки
- Java 8+: CAS для пустых корзин (lock-free) + synchronized только на корзину при коллизиях (вместо Segment locking из Java 7)
- Lock-free чтение через volatile: table, Node.val, Node.next — get() без блокировок
- null запрещены (ключи и значения) — защита от неоднозначности (null = ключ отсутствует vs null = значение)
- Итераторы weakly consistent — не бросают ConcurrentModificationException, могут не видеть последние изменения
- Parallel resizing: ForwardingNode + helpTransfer() — потоки-писатели помогают ресайзить
- CounterCells (как LongAdder) — каждый поток инкрементирует свою ячейку, size() = сумма
- size() приблизительный, mappingCount() > 2^31 элементов
Частые уточняющие вопросы:
- Чем CHM отличается от Collections.synchronizedMap? — synchronizedMap блокирует ВСЮ мапу на каждую операцию. CHM блокирует только одну корзину (synchronized) или не блокирует вовсе (CAS).
- Почему null запрещены в CHM? — Чтобы отличить «ключа нет» (get = null) от «значение = null». В HashMap это невозможно, что создаёт неоднозначность.
- Что будет при долгой операции в computeIfAbsent? — Блокировка корзины на всё время вычисления. Другие потоки, пишущие в ту же корзину, будут ждать. Рекурсивный доступ → deadlock.
- Как CHM ресайзится без полной блокировки? — Выставляет ForwardingNode в старых корзинах. Потоки-писатели, видя его, вызывают helpTransfer() и помогают переносить данные.
Красные флаги (НЕ говорить):
- ❌ «CHM использует Segment locking» — это Java 7, в Java 8+ node-level locking
- ❌ «CHM полностью блокирует мапу при записи» — блокирует только одну корзину, остальные работают
- ❌ «CHM поддерживает null значения» — нет, NullPointerException для null ключей и значений
- ❌ «size() в CHM точный» — приблизительный, может измениться сразу после вызова
Связанные темы:
- [[19. Как ConcurrentHashMap обеспечивает thread-safety]]
- [[14. Что такое Map и какие реализации существуют]]
- [[20. Что такое CopyOnWriteArrayList]]
- [[15. В чём разница между HashMap, LinkedHashMap и TreeMap]]