В чем разница между методами put() и compute() в ConcurrentHashMap?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Разница между 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 может привести к потере данных.