Вопрос 22 · Раздел 10

Как работает HashMap в многопоточной среде?

В Java 7 при параллельном resize элементы списка разворачивались (head insertion). Два потока могли создать циклическую ссылку → бесконечный цикл при get() → 100% CPU.

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

🟢 Junior Level

HashMap не является потокобезопасной. Если несколько потоков одновременно обращаются к HashMap, могут возникнуть проблемы:

  1. Потеря данных — один поток может перезаписать результат другого
  2. Непредсказуемое поведениеget() может вернуть не то значение
  3. ConcurrentModificationException — при итерации и одновременной модификации

Пример проблемы:

Map<String, Integer> map = new HashMap<>();

// Основной риск — corruption при resize, не при обычной вставке
// Если ключи разные и бакеты разные — потеря маловероятна
// Но при конкурентном resize возможна потеря элементов!
// Хуже: если ключи ОДИНАКОВЫ:
// Поток 1: put("key", 100)
// Поток 2: put("key", 200) → перезаписывает!

Решение: Используйте ConcurrentHashMap для многопоточного доступа.

🟡 Middle Level

Основные риски

Риск Описание Последствия
Race Condition Два потока пишут в один бакет Потеря данных
Visibility Изменения не видны другим потокам Устаревшие данные
Resize Два потока одновременно расширяют Потеря данных, corruption
Fail-Fast Iterator Модификация во время итерации ConcurrentModificationException

Проблема Java 7: Infinite Loop

В Java 7 при параллельном resize элементы списка разворачивались (head insertion). Два потока могли создать циклическую ссылку → бесконечный цикл при get() → 100% CPU.

В Java 8+ это исправлено через tail insertion, но потеря данных при resize всё ещё возможна.

Как работать безопасно?

Способ Механизм Производительность
ConcurrentHashMap CAS + блокировка на уровне бакета Высокая
Collections.synchronizedMap() Один мьютекс на всё Низкая
Hashtable Синхронизированные методы Низкая (устарело)

Когда HashMap можно использовать в многопоточности?

Только если она заполнена до запуска потоков и используется только для чтения. Важно: нужна safe publication — без volatile/final другой поток может не увидеть записанные данные.

Map<String, Integer> map = new HashMap<>();
map.put("config", 42);
// Запускаем потоки — только read!

Типичные ошибки

  1. Думать, что fail-fast = безопасность — это only best-effort detection
  2. Использовать HashMap в Spring Singleton — один инстанс на все запросы
  3. Проверка-потом-действие без атомарности — TOCTOU race condition

🔴 Senior Level

Internal Race Conditions

// putVal — упрощённый код:
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

Два потока одновременно:

  1. Оба видят tab[i] == null
  2. Оба создают newNode
  3. Оба записывают в tab[i] — один теряет элемент

Без синхронизации это data race по Java Memory Model.

Visibility Problem

Поля HashMap не volatile. Даже если поток A записал элемент, поток B может не увидеть его в своём CPU cache (L1/L2). Нужен memory barrier.

Resize Race Condition

// Два потока одновременно вызывают resize():
// 1. Оба создают newTab
// 2. Оба копируют элементы
// 3. Оба записывают `table = newTab`
// Результат: один массив теряет элементы

Fail-Fast Mechanism

transient int modCount; // Увеличивается при каждой модификации

// В итераторе:
int expectedModCount = modCount;
public V next() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

Это не механизм синхронизации, а only debugging aid.

Safe Publication Pattern

HashMap безопасна для чтения после safe publication:

// Через final поле:
private final Map<K, V> map = createAndFillMap();

// Через volatile:
private volatile Map<K, V> map;

// Через AtomicReference:
private final AtomicReference<Map<K, V>> mapRef;

ConcurrentHashMap: Internal Architecture

Java 7: Segment[] (16 сегментов, каждый со своей блокировкой)
Java 8+: Node-level locking (synchronized на голове бакета) + CAS

В Java 8+:

  • CAS для пустого бакета (без блокировки)
  • synchronized на голове бакета для коллизий
  • Видимость обеспечивается через Unsafe.compareAndSwap (CAS-операции) и memory barriers, а не через volatile-декларации полей Node.

Production Experience

В Spring-приложениях HashMap как поле @Service — частый баг:

@Service
public class CacheService {
    private Map<String, Data> cache = new HashMap<>(); // НЕБЕЗОПАСНО!
}
// Множество HTTP-запросов = множество потоков

Решение: ConcurrentHashMap или Collections.synchronizedMap().


🎯 Шпаргалка для интервью

Обязательно знать:

  • HashMap НЕ потокобезопасна: data race, потеря данных, corruption при resize
  • Safe publication: заполненная до запуска потоков Map нужна volatile/final для видимости
  • Fail-fast iterator (modCount) — это debugging aid, НЕ механизм синхронизации
  • Java 7: infinite loop при параллельном resize (head insertion); Java 8+: исправлено (tail insertion)
  • Три safe варианта: ConcurrentHashMap (CAS + блокировка бакета), synchronizedMap, Hashtable
  • ConcurrentModificationException не гарантирует обнаружение всех гонок

Частые уточняющие вопросы:

  • Можно ли читать HashMap из нескольких потоков? — да, если safe publication (final/volatile) и никто не пишет
  • Почему Spring @Service с HashMap — баг? — один инстанс на все HTTP-запросы (много потоков)
  • Что такое TOCTOU race? — Time-Of-Check-Time-Of-Use: проверка и действие не атомарны
  • Видимость без volatile? — другой поток может не увидеть записанные данные (CPU cache)

Красные флаги (НЕ говорить):

  • «Fail-fast = потокобезопасность» — нет, только detection
  • «HashMap безопасна если ключи разные» — нет, corruption при resize возможна
  • «synchronizedMap = быстрое решение» — нет, один мьютекс на всё, низкая производительность

Связанные темы:

  • [[21. Когда временная сложность может стать O(n)]]
  • [[23. Что такое ConcurrentHashMap и чем он отличается от HashMap]]
  • [[19. Что происходит при rehashing]]