Вопрос 18 · Раздел 4

Что такое ConcurrentHashMap?

В отличие от Collections.synchronizedMap(new HashMap<>()) (одна блокировка на всю мапу), CHM позволяет множеству потоков работать параллельно, блокируя только отдельные корзины.

Версии по языкам: English Russian Ukrainian

🟢 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

  1. По умолчанию для многопоточности
  2. null запрещён — защита от неоднозначности
  3. computeIfAbsent — атомарная инициализация
  4. Не долгие операции в compute
  5. mappingCount() > size() для > 2^31
  6. Bad hashCode → главный враг параллелизма
  7. 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]]