Як HashMap працює в багатопотоковому середовищі?
В Java 7 при паралельному resize елементи списку розгорталися (head insertion). Два потоки могли створити циклічне посилання → нескінченний цикл при get() → 100% CPU.
🟢 Junior Level
HashMap не є потокобезпечною. Якщо кілька потоків одночасно звертаються до HashMap, можуть виникнути проблеми:
- Втрата даних — один потік може перезаписати результат іншого
- Непередбачувана поведінка —
get()може повернути не те значення - 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!
Типові помилки
- Думати, що fail-fast = безпека — це only best-effort detection
- Використовувати HashMap в Spring Singleton — один інстанс на всі запити
- Перевірка-потім-дія без атомарності — TOCTOU race condition
🔴 Senior Level
Internal Race Conditions
// putVal — спрощений код:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
Два потоки одночасно:
- Обидва бачать
tab[i] == null - Обидва створюють
newNode - Обидва записують в
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)
- Три безпечних варіанти: 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]]