← Назад к вопросам
Какие плюсы и минусы ConcurrentHashMap?
2.2 Middle🔥 201 комментариев
#Коллекции#Многопоточность
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Плюсы и минусы ConcurrentHashMap
ConcurrentHashMap — это thread-safe реализация HashMap в Java, которая обеспечивает безопасность при параллельном доступе нескольких потоков. Это важная часть java.util.concurrent пакета для многопоточного программирования.
Проблема: обычный HashMap в многопоточности
// Проблема 1: HashMap НЕ thread-safe
Map<String, Integer> map = new HashMap<>();
// Поток 1
map.put("key1", 1);
// Поток 2
map.put("key2", 2);
// Возможна race condition, corruption данных!
// Неправильное решение: synchronized
Map<String, Integer> syncMap = Collections.synchronizedMap(
new HashMap<>()
);
// Весь map блокируется при КАЖДОЙ операции
// Плохая производительность!
Как работает ConcurrentHashMap
Вместо одного lock'а для всего map используются multiple lock'и — по одному на сегмент (bucket)
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Поток 1
map.put("key1", 1); // Lock на segment для bucket hash("key1")
// Поток 2
map.put("key2", 2); // Lock на segment для bucket hash("key2")
// Если они в разных segments → параллельно!
// Поток 3
Integer value = map.get("key1"); // Может даже не блокировать
Плюсы ConcurrentHashMap
1. Высокая производительность в многопоточности
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Multiple потоки могут писать одновременно
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
final int index = i;
executor.submit(() -> {
map.put("key" + index, index);
// Не блокирует всю карту — только один segment
});
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
Сравнение производительности:
// HashMap + synchronized (МЕДЛЕННО)
Map<String, Integer> syncedMap = Collections.synchronizedMap(
new HashMap<>()
);
// Каждая операция блокирует весь map
// 10 потоков = по сути работает 1 поток
// ConcurrentHashMap (БЫСТРО)
ConcurrentHashMap<String, Integer> concMap =
new ConcurrentHashMap<>();
// 10 потоков могут работать параллельно
// на разных segments
2. Read операции обычно не требуют lock'а
ConcurrentHashMap<String, String> map =
new ConcurrentHashMap<>();
// Многие операции чтения не блокируют
String value = map.get("key"); // Часто без lock'а
bool exists = map.containsKey("key"); // Без lock'а
// Это возможно потому что используется volatile
// и happens-before отношения
3. Атомарные составные операции
// putIfAbsent — атомарная операция
String oldValue = map.putIfAbsent("key", "new value");
if (oldValue == null) {
System.out.println("Was not present");
}
// Без ConcurrentHashMap нужно было бы:
if (!syncedMap.containsKey("key")) { // Check
syncedMap.put("key", "value"); // Act
// RACE CONDITION: между Check и Act
}
// Другие атомарные методы
map.putIfAbsent(key, value);
map.replace(key, oldValue, newValue);
map.remove(key, value);
map.compute(key, (k, v) -> newValue);
4. Итерирование без копирования
ConcurrentHashMap<String, Integer> map =
new ConcurrentHashMap<>();
for (int i = 0; i < 1000; i++) {
map.put("key" + i, i);
}
// Не нужно создавать копию для итерации
for (Map.Entry<String, Integer> entry : map.entrySet()) {
// Можно добавлять/удалять элементы во время итерации
// (не будет ConcurrentModificationException)
if (entry.getValue() > 500) {
map.remove(entry.getKey());
}
}
5. Поддержка computeIfXXX методов
ConcurrentHashMap<String, Integer> map =
new ConcurrentHashMap<>();
// Атомарное вычисление значения если ключа нет
Integer count = map.computeIfAbsent("key", k -> {
// Это вычислится ОДИН раз даже если множество потоков
return database.query(k).count();
});
// Безопасное обновление
map.compute("counter", (k, v) -> (v == null ? 1 : v + 1));
// Обновление если значение не совпадает
map.computeIfPresent("key", (k, v) -> v + 1);
Минусы ConcurrentHashMap
1. Не гарантирует консистентность с внешним состоянием
ConcurrentHashMap<String, Account> accounts =
new ConcurrentHashMap<>();
// ПРОБЛЕМА: check-then-act race condition
if (!accounts.containsKey("john")) { // Check
// Другой поток может добавить "john" сюда!
accounts.put("john", new Account("john")); // Act
// Потенциально создали дубль
}
// РЕШЕНИЕ: используй putIfAbsent
accounts.putIfAbsent("john", new Account("john"));
2. Итерирование может показать частичные обновления
ConcurrentHashMap<String, Integer> map =
new ConcurrentHashMap<>();
for (int i = 0; i < 100; i++) {
map.put("key" + i, i);
}
// Поток 1: итерирует
int total = 0;
for (Integer value : map.values()) {
total += value; // Может быть inconsistent snapshot
}
// Поток 2: изменяет
for (int i = 0; i < 100; i++) {
map.put("key" + i, i * 2);
}
// Поток 1 может увидеть mix старых и новых значений
3. Size() и isEmpty() могут быть неточными
ConcurrentHashMap<String, Integer> map =
new ConcurrentHashMap<>();
// size() подсчитывает segments, может быть off-by-one
int size = map.size(); // Может быть неточно!
// Потому что размер изменяется во время подсчёта
// Разные потоки добавляют/удаляют элементы
4. Больше памяти чем HashMap
// ConcurrentHashMap использует несколько lock'ов
ConcurrentHashMap<String, Integer> concMap =
new ConcurrentHashMap<>(); // Больше памяти
// HashMap с synchronized обёрткой
Map<String, Integer> syncMap =
Collections.synchronizedMap(new HashMap<>()); // Меньше памяти
5. Не поддерживает null ключи и значения
ConcurrentHashMap<String, Integer> map =
new ConcurrentHashMap<>();
// HashMap позволяет
HashMap<String, Integer> hm = new HashMap<>();
hm.put(null, 1); // OK
hm.put("key", null); // OK
// ConcurrentHashMap НЕ позволяет
map.put(null, 1); // Выброс NullPointerException
map.put("key", null); // Выброс NullPointerException
6. Производительность при частых переразмещениях (rehash)
ConcurrentHashMap<String, Integer> map =
new ConcurrentHashMap<>(1); // Маленький initial size
// Много добавлений → много rehash'ей
// ConcurrentHashMap дорогие rehash'и
for (int i = 0; i < 100000; i++) {
map.put("key" + i, i);
}
// Лучше указать initial capacity
ConcurrentHashMap<String, Integer> map2 =
new ConcurrentHashMap<>(100000);
for (int i = 0; i < 100000; i++) {
map2.put("key" + i, i);
}
Сравнение решений
| Решение | Thread-Safe | Производительность | Памяти | null | Когда использовать |
|---|---|---|---|---|---|
| HashMap | Нет | Очень высокая | Низкая | Да | Single thread |
| synchronized HashMap | Да | Низкая | Низкая | Да | Мало потоков |
| Collections.synchronizedMap | Да | Низкая | Низкая | Да | Мало потоков |
| ConcurrentHashMap | Да | Очень высокая | Средняя | Нет | Много потоков |
| ConcurrentSkipListMap | Да | Средняя | Средняя | Нет | Нужен порядок |
Практические примеры
Правильное использование
public class UserCache {
private ConcurrentHashMap<String, User> cache =
new ConcurrentHashMap<>();
public User getOrFetch(String userId) {
// Атомарно получить или вычислить
return cache.computeIfAbsent(userId, id -> {
// Вычислить один раз даже если множество потоков
return fetchUserFromDatabase(id);
});
}
public void invalidate(String userId) {
cache.remove(userId);
}
}
Счётчик с ConcurrentHashMap
public class ThreadSafeCounter {
private ConcurrentHashMap<String, AtomicInteger> counters =
new ConcurrentHashMap<>();
public void increment(String key) {
// Неправильно
// if (!counters.containsKey(key)) {
// counters.put(key, new AtomicInteger(0));
// }
// counters.get(key).incrementAndGet();
// RACE CONDITION!
// Правильно
counters.computeIfAbsent(key, k ->
new AtomicInteger(0)
).incrementAndGet();
}
public int get(String key) {
AtomicInteger counter = counters.get(key);
return counter != null ? counter.get() : 0;
}
}
Session Management
public class SessionManager {
private ConcurrentHashMap<String, Session> sessions =
new ConcurrentHashMap<>();
public Session getOrCreateSession(String sessionId) {
return sessions.computeIfAbsent(sessionId, id -> {
System.out.println("Creating new session: " + id);
return new Session(id);
});
}
public void removeSession(String sessionId) {
sessions.remove(sessionId);
}
// Можно итерировать безопасно
public void expireSessions() {
sessions.forEachValue(1, session -> {
if (session.isExpired()) {
sessions.remove(session.getId());
}
});
}
}
Когда выбрать ConcurrentHashMap
✅ Используй ConcurrentHashMap когда:
- Много потоков читают и пишут одновременно
- Нужна высокая производительность
- Не нужны null ключи/значения
- Не нужна полная консистентность итерирования
❌ НЕ используй ConcurrentHashMap когда:
- Single-threaded приложение (используй HashMap)
- Нужны null значения (используй ImmutableMap с Optional)
- Критична абсолютная консистентность (рассмотри другой подход)
- Очень мало элементов (overhead не стоит)
Лучшие практики
- Используй computeIfAbsent вместо check-then-act
// Плохо
if (!map.containsKey(key)) {
map.put(key, value);
}
// Хорошо
map.putIfAbsent(key, value);
map.computeIfAbsent(key, k -> value);
- Указывай initial capacity
ConcurrentHashMap<String, Integer> map =
new ConcurrentHashMap<>(expectedSize);
- Используй lambda вместо multiple операций
// Плохо
Integer count = map.get("counter");
if (count == null) {
map.put("counter", 1);
} else {
map.put("counter", count + 1);
}
// Хорошо
map.compute("counter", (k, v) -> (v == null ? 1 : v + 1));
- Не полагайся на size() для логики
// size() может быть неточен
int size = map.size();