Что такое ConcurrentHashMap и чем он отличается от HashMap?
В многопоточной среде get(key) = null неоднозначно:
🟢 Junior Level
ConcurrentHashMap — это потокобезопасная версия HashMap. Несколько потоков могут одновременно читать и записывать данные без проблем.
Главные отличия:
| HashMap | ConcurrentHashMap |
|---|---|
| Не потокобезопасна | Потокобезопасна |
| Быстрее в одном потоке | Чуть медленнее, но безопасна |
| Разрешает null ключи/значения | Запрещает null |
| Fail-fast итератор | Weakly Consistent итератор (не бросает CME) |
Пример:
// HashMap — упадёт в многопоточной среде
Map<String, Integer> unsafe = new HashMap<>();
// ConcurrentHashMap — безопасно
ConcurrentMap<String, Integer> safe = new ConcurrentHashMap<>();
safe.put("key1", 100);
safe.putIfAbsent("key2", 200); // Атомарная операция
Когда использовать: Когда к карте обращаются несколько потоков одновременно.
🟡 Middle Level
Архитектура ConcurrentHashMap (Java 8+)
| Характеристика | HashMap | ConcurrentHashMap |
|---|---|---|
| Потокобезопасность | Нет | Да |
| Блокировки | Нет | Побакетные (synchronized на голове бакета) |
| Null-ключи/значения | Разрешены | Запрещены (NPE) |
| Итератор | Fail-fast | Weakly Consistent (snapshot на момент создания) |
| Атомарные операции | Нет | putIfAbsent, computeIfAbsent, merge |
Как работает потокобезопасность
- CAS (Compare-And-Swap) — для пустого бакета блокировка не нужна
- synchronized на голове бакета — только при коллизиях
- volatile поля — гарантии видимости между потоками
// CAS для пустого бакета:
if (casTabAt(tab, i, null, newNode))
break; // Успешно, без блокировки!
// synchronized при коллизии:
synchronized (f) {
// Добавление в список/дерево
}
Почему запрещён null?
В многопоточной среде get(key) = null неоднозначно:
- Ключа нет? Или значение = null?
- Нельзя атомарно проверить
containsKey()послеget()— состояние может измениться
Атомарные операции
map.putIfAbsent("key", 100); // Проверка + вставка = атомарно
map.computeIfAbsent("key", k -> computeExpensive(k)); // Ленивое вычисление
map.merge("key", 1, Integer::sum); // Атомарное обновление
Итерация: Weakly Consistent
for (Map.Entry<String, Integer> e : map.entrySet()) {
// Не бросит ConcurrentModificationException
// Может не увидеть изменения, сделанные после создания итератора
}
🔴 Senior Level
Internal Implementation (Java 8+)
transient volatile Node<K,V>[] table;
Ключевые отличия от HashMap:
volatileмассив — гарантии видимостиvolatileполя Node (val,next) — happens-before- ForwardingNode при resize — сигнализирует о прогрессе
Multi-threaded Resize
// При resize несколько потоков могут помогать:
private transient volatile int transferIndex;
Потоки координируются через transferIndex, каждый обрабатывает свой диапазон бакетов. Это ускоряет resize в высоконагруженных системах.
Lock Striping vs Node-level Locking
| Подход | Java 7 (Segment) | Java 8+ (Node) |
|---|---|---|
| Гранулярность | 16 сегментов | Каждый бакет |
| Конкуренция | До 16 потоков | До capacity потоков |
| Overhead | Меньше | Минимальный |
Memory Ordering
ConcurrentHashMap использует full memory barrier через volatile:
- Запись в
valвидна всем потокам - Happens-before между put и get из разных потоков
- Никаких stale reads
Why No Null?
Liveness Problem в многопоточности:
// Потенциальный сценарий без запрета null:
if (map.get(key) == null) { // null или нет ключа?
if (!map.containsKey(key)) { // Ключа нет... или он удалён между вызовами?
map.put(key, value); // TOCTOU race!
}
}
Benchmark
| Метрика | HashMap | ConcurrentHashMap |
|---|---|---|
| get (1 thread) | 5 ns | 8 ns |
| get (8 threads) | N/A (race) | 60 ns |
| put (1 thread) | 8 ns | 15 ns |
| put (8 threads) | N/A (race) | 120 ns |
Overhead ~60% на single-thread, но на 8 threads — единственный корректный вариант.
Production Best Practices
- Default choice для многопоточности — ConcurrentHashMap
- Атомарные операции —
compute,mergeвместо check-then-act - size() в Java 8+ использует систему CounterCell и работает за O(1) в обычном случае. Результат точный, но может устареть к моменту использования — не используйте в hot path.
- Bulk operations —
forEach,search,reduce(Java 8+) с parallelism threshold
🎯 Шпаргалка для интервью
Обязательно знать:
- ConcurrentHashMap — потокобезопасна через CAS + synchronized на голове бакета
- Запрещает null-ключи/значения — устраняет ambiguity в check-then-act
- Weakly Consistent итератор — snapshot на момент создания, не бросает CME
- Атомарные операции: putIfAbsent, computeIfAbsent, merge — заменяют check-then-act
- Java 8+: node-level locking (ранее Java 7: segment-level, 16 сегментов)
- size() через CounterCell: O(1), точный но может устареть
- Multi-threaded resize: потоки помогают через transferIndex
Частые уточняющие вопросы:
- Почему запрещён null? — get=null неоднозначно (нет ключа или значение=null?), ломает putIfAbsent
- Чем Weakly Consistent отличается от Fail-safe? — Weakly Consistent: видит изменения после создания, но не гарантирует полноту
- Почему не synchronizedMap? — один мьютекс на всё; CHM = конкурентный доступ к разным бакетам
- Overhead vs HashMap? — ~60% на single-thread, но единственный корректный вариант для multi-thread
Красные флаги (НЕ говорить):
- «ConcurrentHashMap использует volatile поля Node» — нет, CAS + memory barriers
- «Fail-safe итератор» — правильно: Weakly Consistent
- «size() = O(n)» — нет, в Java 8+ через CounterCell = O(1)
Связанные темы:
- [[22. Как работает HashMap в многопоточной среде]]
- [[25. Можно ли хранить null значение в HashMap]]
- [[26. В чём разница между HashMap и Hashtable]]