Как работает блокировка по сегментам (segmentation) в ConcurrentHashMap и зачем она нужна?

«Как работает блокировка по сегментам (segmentation) в ConcurrentHashMap и зачем она нужна?» — вопрос из категории Java Core, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Цель: Обеспечить высокую производительность при конкурентных операциях чтения и записи за счет уменьшения области (гранулярности) блокировки.

Проблема Hashtable/synchronized HashMap: Они используют единую глобальную блокировку на весь объект карты. При одновременной работе нескольких потоков даже с разными ключами они выстраиваются в очередь, что убивает параллелизм.

Решение ConcurrentHashMap (до Java 8): Карта внутренне разделена на сегменты (Segment) — независимые подтаблицы. Каждый сегмент — это по сути небольшая хэш-таблица со своей собственной блокировкой.

Принцип работы (упрощенно):

  1. Ключ хэшируется.
  2. По хэшу определяется, какому сегменту принадлежит ключ.
  3. Для операции с этим ключом блокируется только его сегмент, а не вся карта.
  4. Другие потоки могут свободно работать с другими сегментами.
// Концептуальная иллюстрация структуры (реализация менялась)
ConcurrentHashMap<K, V> map;

// Внутри: массив сегментов (например, 16 штук по умолчанию)
Segment[] segments = new Segment[16];

// Каждый Segment содержит свой небольшой HashEntry[] и ReentrantLock
class Segment {
    HashEntry[] table;
    ReentrantLock lock;
    // ... методы put, get для своей части данных
}

// Операция put блокирует только один сегмент
map.put(key, value); // Блокируется Segment[hash(key) % segments.length]

Эволюция в Java 8 и новее: Модель с сегментами была заменена на более эффективную, использующую отдельные synchronized-блоки на головных узлах (первых узлах) бакетов (bucket) и CAS-операции (Compare-And-Swap). Однако логика осталась схожей: блокируется не вся таблица, а только небольшая ее часть (один бакет).

Ключевые преимущества подхода с мелкогранулярными блокировками:

  • Высокий параллелизм при записи: Потоки, работающие с ключами из разных сегментов/бакетов, не блокируют друг друга.
  • Неблокирующее чтение: Операции get() и обход итератором (в большинстве случаев) не требуют блокировок, так как используют volatile-поля и безопасную публикацию узлов, что делает их очень быстрыми.
  • Предсказуемая производительность: Конфликты блокировок сведены к минимуму.

Итог: Блокировка по сегментам/бакетам — это фундаментальный паттерн в ConcurrentHashMap, который позволяет ей превосходить по производительности синхронизированные аналоги в многопоточной среде, обеспечивая при этом потокобезопасность.