Исправить конкурентную запись в ConcurrentHashMap

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().