Что такое race condition?
Состояние гонки (race condition) – это ошибка проектирования многопоточной программы, при которой корректность работы зависит от порядка выполнения потоков. Поведение программы становится недетерминированным: при одном порядке выполнения результат правильный, при другом – ошибочный.
Два типа race condition
| Тип | Описание | Пример |
|---|---|---|
| Check-then-act | Проверка условия и действие на его основе – не атомарны | if (map.containsKey(key)) map.get(key) |
| Read-modify-write | Чтение, изменение и запись значения – не атомарны | count++ |
Пример: check-then-act
Пример: гонка при ленивой инициализации
public class LazyInitRace {
private static ExpensiveObject instance;
// НЕ потокобезопасно!
public static ExpensiveObject getInstance() {
if (instance == null) { // Поток A проверяет — null
// Поток B тоже проверяет — null
instance = new ExpensiveObject(); // Оба создают экземпляр!
}
return instance;
}
}
Пример: read-modify-write
Пример: гонка при инкременте счётчика
public class CounterRace {
private int count = 0;
// НЕ потокобезопасно!
public void increment() {
count++; // Три операции: read → modify → write
}
// Возможный сценарий при count = 5:
// Поток A: read count → 5
// Поток B: read count → 5
// Поток A: write count → 6
// Поток B: write count → 6 ← Должно быть 7!
}
Почему race condition опасен
- Недетерминированность – баг может проявляться один раз на тысячу запусков, что делает его крайне сложным для воспроизведения.
- Зависимость от окружения – на машине разработчика (1 ядро) баг не воспроизводится, а на production-сервере (64 ядра) – постоянно.
- Каскадные последствия – повреждение данных, нарушение инвариантов, «невозможные» состояния объектов.
Аналогия из жизни. Два человека одновременно проверяют, есть ли молоко в холодильнике, видят, что нет, и оба идут в магазин. В итоге – два пакета молока вместо одного. «Проверил – пусто» и «пошёл купить» – не атомарная операция.
На собеседовании. Назовите два типа race condition: check-then-act и read-modify-write. Приведите конкретный пример кода. Подчеркните, что race condition проявляется недетерминированно и зависит от таймингов – это делает его одним из самых сложных классов багов. Также упомяните инструменты обнаружения: Thread Sanitizer (для native кода), JCStress (для Java-кода), а также статический анализ (FindBugs/SpotBugs с детектором
MT_CORRECTNESS).