Как правильно реализовать увеличение счетчика из двух потоков в Java для корректного значения
Что проверяют
Понимаете ли вы, почему count++ из двух потоков теряет обновления и чем атомарность отличается от видимости. Следом почти всегда спрашивают, поможет ли volatile и что выбрать при сильной конкуренции.
Как отвечать
Начните с причины: count++ это три действия, чтение, прибавление и запись. Два потока читают одно и то же значение, оба записывают «плюс один», и одно увеличение пропадает. volatile гарантирует, что поток увидит свежее значение, но три шага в один не склеивает, поэтому здесь он не спасает.
Рабочие варианты:
synchronized: инкремент и чтение под одним монитором. Даёт и атомарность, и happens-before между потоками. Геттер тоже должен бытьsynchronized, иначе читатель может увидеть устаревшее значение;AtomicInteger:incrementAndGet()без блокировок, на CAS-инструкциях процессора. Для простого счётчика это ответ по умолчанию;LongAdder: при сильной конкуренции раскладывает прибавления по нескольким ячейкам, и потоки почти не спорят за одну переменную. Цена:sum()под нагрузкой не точный мгновенный снимок, поэтому он хорош для метрик, а не для выдачи уникальных номеров;ReentrantLock: когда нужныtryLock()с таймаутом или справедливая очередь.unlock()всегда вfinally.
Результат читают только после завершения потоков: join(), CountDownLatch или ExecutorService с shutdown() и awaitTermination(). Хорошо добавить, что гонки ловят не одним прогоном, а стресс-тестом или инструментом jcstress.
Где спотыкаются
Отвечают «сделаю поле volatile». Видимость появится, а потерянные инкременты останутся.
Синхронизируют не на том объекте: synchronized (new Object()) внутри метода или разные экземпляры счётчика в разных потоках. Каждый поток берёт свой монитор, и защиты нет.
Проверяют решение циклом на тысячу итераций и видят правильную сумму. На маленьком объёме гонка может не проявиться ни разу, поэтому «у меня всё сошлось» ничего не доказывает.