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

В чем разница между методами put() и compute() в ConcurrentHashMap?

2.7 Senior🔥 111 комментариев
#Коллекции#Многопоточность#Основы Java

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Разница между put() и compute() в ConcurrentHashMap

Это два различных подхода к работе с элементами в ConcurrentHashMap. Они отличаются поведением, потокобезопасностью и производительностью.

Метод put()

Метод put() выполняет две отдельные операции: сначала читает значение, потом записывает:

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("count", 1);

// put() имеет сигнатуру
V put(K key, V value);

// Использование
Integer oldValue = map.put("count", 2);  // Заменяет значение на 2

Основные характеристики put():

  • Просто добавляет или заменяет значение
  • Возвращает старое значение (или null, если ключа не было)
  • Состоит из двух операций: lock и write
  • Не гарантирует атомарность сложных операций

Метод compute()

Метод compute() выполняет атомарную операцию: читает значение и пересчитывает его в одном блокировочном интервале:

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

// compute() имеет сигнатуру
V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction);

// Использование: увеличить значение или установить 1 если ключа нет
map.compute("count", (key, oldValue) -> oldValue == null ? 1 : oldValue + 1);

Основные характеристики compute():

  • Функция выполняется внутри блокировки
  • Атомарно: lock + read + compute + write
  • Функция может видеть текущее значение
  • Исключает race condition

Практическое сравнение

1. Race Condition при использовании put()

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("count", 0);

// ОПАСНО! Race condition
Integer current = map.get("count");  // Получить текущее значение
map.put("count", current + 1);       // Увеличить и сохранить

// Сценарий проблемы:
// Поток 1: get() -> 5
// Поток 2: get() -> 5
// Поток 1: put(6)
// Поток 2: put(6)  <- Lost update!
// Результат: 6 (вместо 7)

2. Атомарность при использовании compute()

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("count", 0);

// БЕЗОПАСНО! Атомарная операция
map.compute("count", (key, value) -> value == null ? 1 : value + 1);

// Гарантирует: read + compute + write как одна операция
// Никакой другой поток не может помешать

Примеры использования

Обновление счётчика

// Используя put() — требует дополнительной синхронизации
public void incrementCountBad(ConcurrentHashMap<String, Integer> map, String key) {
    Integer value = map.getOrDefault(key, 0);
    map.put(key, value + 1);
}

// Используя compute() — атомарно
public void incrementCountGood(ConcurrentHashMap<String, Integer> map, String key) {
    map.compute(key, (k, v) -> v == null ? 1 : v + 1);
}

Кэширование результата функции

ConcurrentHashMap<Integer, String> cache = new ConcurrentHashMap<>();

// ПЛОХО: cache miss вызовет compute дважды из разных потоков
String value = cache.get(key);
if (value == null) {
    value = expensiveComputation(key);
    cache.put(key, value);
}

// ХОРОШО: expensiveComputation выполнится ровно один раз
String value = cache.compute(key, (k, v) -> 
    v != null ? v : expensiveComputation(k)
);

Агрегирование данных

ConcurrentHashMap<String, List<String>> groups = new ConcurrentHashMap<>();

// Добавить элемент в группу — атомарно
groups.compute("group1", (key, list) -> {
    if (list == null) {
        list = new ArrayList<>();
    }
    list.add("item");
    return list;
});

Похожие методы

computeIfAbsent() — compute только если ключа нет

// Получить или создать значение
map.computeIfAbsent("key", k -> new ArrayList<>());

computeIfPresent() — compute только если ключ есть

// Обновить только существующие значения
map.computeIfPresent("key", (k, v) -> v + 1);

merge() — слить два значения

// Слить значения или создать новое
map.merge("count", 1, Integer::sum);

Сравнение производительности

// Сценарий: 1000 потоков, каждый делает 10000 операций инкремента

// Используя put() + get() — медленно, много lock/unlock
ExecutorService executor = Executors.newFixedThreadPool(1000);
for (int i = 0; i < 1000; i++) {
    executor.submit(() -> {
        for (int j = 0; j < 10000; j++) {
            Integer v = map.get("count");
            map.put("count", v == null ? 1 : v + 1);  // ПЛОХО
        }
    });
}
// Время: ~2000ms, результат неправильный

// Используя compute() — быстро, один lock/unlock
for (int i = 0; i < 1000; i++) {
    executor.submit(() -> {
        for (int j = 0; j < 10000; j++) {
            map.compute("count", (k, v) -> v == null ? 1 : v + 1);  // ХОРОШО
        }
    });
}
// Время: ~800ms, результат всегда правильный

Итог

Выбор между put() и compute() в ConcurrentHashMap должен быть очень внимательным. put() подходит только для простого чтения и записи отдельных значений. compute() необходимо использовать, когда нужно атомарно прочитать старое значение и вычислить новое. Это особенно важно в многопоточных приложениях, где race condition может привести к потере данных.