Что происходит при запуске двух потоков, инкрементирующих одну переменную, и почему
Что проверяют
Понимаете ли вы модель памяти Java, а не только слово «гонка». Полный ответ содержит две разные проблемы: неатомарность инкремента и видимость записей между потоками. Кандидат, который знает только первую, потом ставит volatile там, где нужен замок.
Как отвечать
count++ это три действия: чтение поля, прибавление, запись. Между ними поток могут вытеснить, поэтому два потока читают одно и то же значение, оба записывают на единицу больше, и один инкремент теряется. Итог непредсказуем и никогда не больше суммы итераций.
Вторая половина ответа про видимость. Без отношения happens-before компилятор и процессор вправе не публиковать запись другому потоку и вправе поднять чтение поля из цикла в регистр, поэтому поток может крутиться вечно на устаревшем значении.
Дальше перечисляются инструменты и их цена:
synchronizedиReentrantLock: взаимное исключение плюс видимость, но потоки блокируются;AtomicInteger.incrementAndGet(): атомарность через CAS без блокировки, дешевле при умеренной конкуренции;LongAdder: разносит счётчик по ячейкам и выигрывает при высокой конкуренции, значение читается черезsum();volatile int: только видимость, инкремент не чинит.
private final AtomicInteger count = new AtomicInteger();
count.incrementAndGet();Где спотыкаются
Считают volatile заменой синхронизации. Он даёт видимость и запрещает переупорядочивание, но ++ остаётся тремя операциями, и потеря обновлений никуда не девается.
Второе: синхронизируются на разных мониторах. synchronized на обычном методе берёт замок экземпляра, у статического метода замок класса; если каждый поток работает со своим объектом или замок берётся на поле, которое переприсваивают внутри блока, защиты нет вовсе.
Третье: проверяют многопоточность одним прогоном. На небольшом числе итераций результат часто сходится, и код уезжает в прод. Гонку ловят нагрузкой, повторами и инструментами вроде jcstress, а не чтением глазами.