← Назад к вопросам

Какие плюсы и минусы 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 не стоит)

Лучшие практики

  1. Используй computeIfAbsent вместо check-then-act
// Плохо
if (!map.containsKey(key)) {
    map.put(key, value);
}

// Хорошо
map.putIfAbsent(key, value);
map.computeIfAbsent(key, k -> value);
  1. Указывай initial capacity
ConcurrentHashMap<String, Integer> map = 
    new ConcurrentHashMap<>(expectedSize);
  1. Используй 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));
  1. Не полагайся на size() для логики
// size() может быть неточен
int size = map.size();
Какие плюсы и минусы ConcurrentHashMap? | PrepBro