Как сохранить атомарность с помощью методов get и put ConcurrentHashMap?

В многопоточной среде я выполняю операции получения и размещения в реализации ConcurrentHashMap. Однако результаты неожиданны. Пожалуйста, найдите ниже код и вывод.

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Test {

    private static final List<String> bars = Arrays.asList("1","2","3","4","5","6","7","8","9","10");
    private static final String KEY = UUID.randomUUID().toString();
    private static ExecutorService executorService = null;

    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            executorService = Executors.newFixedThreadPool(2);
            Map<String, AtomicInteger> map = new ConcurrentHashMap<>();
            performMapOps(map);
            executorService.shutdown();
            try {
                while (!executorService.awaitTermination(1, TimeUnit.SECONDS));
                System.out.println(map.get(KEY));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private static void performMapOps(Map<String, AtomicInteger> map) {
        for (int i = 1; i <= bars.size(); i++) {
            executorService.execute(() -> ops(map));
        }
    }

    private static void ops(Map<String, AtomicInteger> map) {
       if (!map.containsKey(KEY)) {
            AtomicInteger atomicInteger = new AtomicInteger(1);
            map.put(KEY, atomicInteger);
        } else {
            map.get(KEY).set(map.get(KEY).intValue() + 1);
        }
    }
}

ВЫВОД - всегда должно быть 10, однако это неверно для приведенного выше кода. Пожалуйста, найдите ниже вывод.

10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
9
10
10

Иногда это дает мне значение, отличное от 10. Пожалуйста, помогите понять, почему такое неожиданное поведение и как это исправить?

Вам нужно заменить тело операций на все за одну операцию, например map.computeIfAbsent(KEY, k -> new AtomicInteger()). incrementAndGet().

DuncG 12.05.2023 10:02
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
1
60
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Ваш код имеет состояние гонки. Два потока могут одновременно решить, что !map.containsKey(KEY) ложно, и присвоить карте разные new AtomicInteger(1).

Два потока могут одновременно решить, что !map.containsKey(KEY) истинно, и они оба могут оценить map.get(KEY) как одно и то же значение, поэтому сохраните одно и то же новое значение с .set(map.get(KEY).intValue() + 1).

Атомарная операция обновления может быть достигнута с помощью Map.computeIfAbsent() с AtomicInteger.incrementAndGet(), чтобы обеспечить последовательное увеличение счетчика без обновления значения другим потоком:

private static void ops(Map<String, AtomicInteger> map) { 
    map.computeIfAbsent(KEY, k -> new AtomicInteger()).incrementAndGet();
}

В исходном коде не было «условия гонки», а было несколько условий гонки. Придирка: во втором разделе сценарий, когда map.get(KEY) возвращает одно и то же значение, не является проблемой, так как это на самом деле задумано (значение, возвращаемое map.get(KEY), является AtomicInteger). Не говоря уже о том, что это не гарантируется из-за проблемы, описанной в первом разделе, фактическая вторая проблема заключается в том, что intValue() может возвращать одно и то же значение. Возможно даже, что во время выполнения потока происходит несколько обновлений intValue() + 1, и в конечном итоге все они откатываются.

Holger 12.05.2023 11:03

Другие вопросы по теме

Почему добавление элементов в ICollectionView из потока пользовательского интерфейса в WPF приводит к ошибке, хотя с ObservableCollection все работает нормально?
Оптимизация ведения журнала в многопоточном серверном приложении C++ для повышения производительности и обеспечения целостности данных
Обновление графика в tkinter с пользовательского ввода без задержек с использованием потоков
Состояние гонки в C
Как два std::unique_locks могут одновременно владеть блокировкой одного и того же мьютекса?
Ошибка Java: исключение в потоке "main" java.lang.IllegalMonitorStateException: текущий поток не является владельцем
Состояние многопоточной гонки Java, минимальный и максимальный диапазон значений
Запрос Rust с тайм-аутом
Valgrind (Helgrind): возможная отладка гонки данных
Найти сумму всех целых чисел в диапазоне [1, n], которые делятся на 3, 5 или 7, используя основы многопоточности в java