Что такое состояние гонки и к чему оно приводит
Что проверяют
Понимаете ли вы, что многопоточная ошибка это не редкий баг, а свойство кода, которое находят рассуждением, а не прогоном. Для Java важна вторая половина вопроса: различаете ли вы гонку данных и состояние гонки и знаете ли модель памяти, из-за которой поток может вовсе не увидеть чужую запись.
Как отвечать
Состояние гонки это ситуация, когда результат зависит от порядка, в котором потоки успели выполнить свои шаги. Гонка данных это её частный случай: два потока обращаются к одному полю, хотя бы один пишет, и между обращениями нет отношения happens-before. По модели памяти Java такая программа корректного поведения не имеет: значение может оказаться не только устаревшим, но и вообще не тем, что записывали.
Два типовых шаблона:
counter++; // читаем, увеличиваем, пишем: три шага, не один
if (map.get(k) == null) // проверили
map.put(k, v); // а решение уже могло устаретьЛечится тем, что составное действие становится неделимым: synchronized на одном и том же мониторе у всех участников, ReentrantLock, AtomicInteger с compareAndSet, атомарные методы ConcurrentHashMap вроде computeIfAbsent. Самый надёжный ход это убрать разделяемое изменяемое состояние: неизменяемые объекты с final полями, локальные копии, передача владения.
Где спотыкаются
Ставят volatile на счётчик и считают задачу закрытой. Ключевое слово чинит видимость и запрещает переупорядочивание, но инкремент остаётся тремя действиями, и потери никуда не денутся.
Синхронизируют запись и оставляют чтение без блокировки. Замок работает, только когда его берут все стороны и на одном объекте, иначе читатель спокойно видит старое значение сколь угодно долго.
Ссылаются на тесты: прогнали тысячу раз, не воспроизвелось. Гонка проявится при другой нагрузке, другом числе ядер и после того, как оптимизатор переставит операции. Доказательством служит разбор инвариантов и happens-before, а зелёный прогон это лишь отсутствие улики.