74. Исправить конкурентную запись в ConcurrentHashMap
Условие задачи:
В коде используется ConcurrentHashMap, но операция «добавить значение, только если ключ отсутствует» реализована некорректно.
Нужно объяснить проблему и исправить код так, чтобы операция была атомарной.
Код:
class Scratch {
public static void main(String[] args) {
var concurrentMap =
new ConcurrentHashMap<String, String>();
new Thread(() ->
putValue(concurrentMap, "key", "value1")
).start();
new Thread(() ->
putValue(concurrentMap, "key", "value2")
).start();
}
private static void putValue(
ConcurrentHashMap<String, String> map,
String key,
String value
) {
if (!map.containsKey(key)) {
map.put(key, value);
}
}
}
Спойлеры к решению
Подсказки
💡
💡 Для операции «положить, если ключ отсутствует» есть атомарный
ConcurrentHashMap делает потокобезопасными отдельные операции, но не последовательность containsKey() + put().💡 Для операции «положить, если ключ отсутствует» есть атомарный
putIfAbsent().Решение
Проблема — race condition:
Thread 1: containsKey → false
Thread 2: containsKey → false
Thread 1: put("key", "value1")
Thread 2: put("key", "value2")
Оба потока проходят проверку, и второй put() может перезаписать значение первого.
Правильный вариант:
class Scratch {
public static void main(String[] args)
throws InterruptedException {
var concurrentMap =
new ConcurrentHashMap<String, String>();
Thread t1 = new Thread(() ->
putValue(concurrentMap, "key", "value1")
);
Thread t2 = new Thread(() ->
putValue(concurrentMap, "key", "value2")
);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(concurrentMap);
}
private static void putValue(
ConcurrentHashMap<String, String> map,
String key,
String value
) {
map.putIfAbsent(key, value);
}
}
putIfAbsent() атомарно выполняет проверку и запись. В мапе останется значение того потока, который первым успешно записал ключ.
Если значение нужно вычислять только при отсутствии ключа, можно использовать:
map.computeIfAbsent(key, k -> value);
Для данной задачи достаточно putIfAbsent().