Race condition
Race condition возникает, когда корректность зависит от непредсказуемого взаимного порядка операций. Data race — более узкое понятие: два потока обращаются к одной памяти без happens-before, хотя бы один пишет.
Потерянное обновление
final class Counter {
private int value;
void increment() {
value++; // read, add, write
}
}Возможное чередование:
| Поток A | Поток B | value |
|---|---|---|
| читает 0 | 0 | |
| читает 0 | 0 | |
| пишет 1 | 1 | |
| пишет 1 | 1 |
Два вызова дали 1, а не 2.
Исправления
Для одного счетчика:
private final AtomicInteger value = new AtomicInteger();
void increment() {
value.incrementAndGet();
}Для составного инварианта:
synchronized void transferTo(Account target, long amount) {
if (balance < amount) throw new IllegalStateException();
balance -= amount;
target.balance += amount;
}Этот пример еще требует общей стратегии порядка lock для разных счетов; synchronized одного source не защищает target.
Concurrent-коллекция защищает отдельные операции, но не check-then-act:
// Гонка:
if (!map.containsKey(key)) {
map.put(key, build());
}
// Атомарная операция коллекции:
map.computeIfAbsent(key, ignored -> build());Даже без data race возможна логическая race: две корректно синхронизированные операции выполнены в допустимом, но нежелательном порядке.
Как искать
- определить общий mutable state и инвариант;
- выписать неделимые с точки зрения бизнеса операции;
- проверить единый happens-before механизм для всех доступов;
- многократный тест повышает шанс проявления, но отсутствие падения ничего не доказывает.
На собеседовании
- Почему
++не атомарен? Это read-modify-write. - Всегда ли race condition равна data race? Нет, логическая гонка возможна и при формально синхронизированном доступе.
- Поможет ли volatile для
++? Нет. - Thread-safe ли
containsKey+putу ConcurrentHashMap? Каждый метод — да, комбинация — нет. - Как исправить check-then-act? Одной атомарной операцией или общей критической секцией.