Как избежать гонок при инкрементации счетчиков в многопоточном коде на Java?
Что проверяют
Понимаете ли вы, почему count++ не атомарен и какие инструменты Java решают это с разной ценой. Следом обычно спрашивают, чем AtomicInteger отличается от synchronized под нагрузкой и когда стоит брать LongAdder.
Как отвечать
Начните с причины: count++ это три операции (прочитать, прибавить, записать), и два потока могут прочитать одно и то же значение, после чего одно приращение потеряется. volatile здесь не спасает: он даёт видимость, но не атомарность составной операции.
Дальше варианты по возрастанию специализации:
synchronized: метод или блок на общем мониторе. Просто и корректно, заодно даёт happens-before, но и чтение должно идти под тем же монитором, иначе геттер увидит устаревшее значение;ReentrantLock: то же взаимное исключение плюсtryLockс таймаутом и прерываемое ожидание. Для голого счётчика избыточен;AtomicIntegerиAtomicLong:incrementAndGet()без блокировок, на CAS-инструкции процессора. При умеренной конкуренции это стандартный выбор;LongAdder: при сильной конкуренции раскладывает счёт по ячейкам для разных потоков, аsum()складывает их. Запись быстрее атомика, ноsum()не даёт точного снимка во время параллельных обновлений;- без общего состояния: каждая задача считает в локальной переменной, а результаты складываются в конце, например из
Future. Синхронизация тогда не нужна вовсе.
Если счётчики лежат в Map, покажите ConcurrentHashMap.merge(key, 1L, Long::sum) или computeIfAbsent(key, k -> new LongAdder()).increment().
Где спотыкаются
Ставят volatile на поле и считают задачу решённой. Инкремент по-прежнему теряется, просто реже воспроизводится на тестах.
Синхронизируют increment(), а getCount() оставляют без синхронизации на обычном int. Без volatile или того же монитора другой поток не обязан увидеть свежее значение.
Пишут if (map.containsKey(k)) map.put(k, map.get(k) + 1) поверх ConcurrentHashMap и считают код потокобезопасным. Каждая операция атомарна по отдельности, а связка «проверить и записать» нет, для неё нужны merge или compute.