Race condition

Race condition возникает, когда корректность зависит от непредсказуемого взаимного порядка операций. Data race — более узкое понятие: два потока обращаются к одной памяти без happens-before, хотя бы один пишет.

Потерянное обновление

final class Counter {
    private int value;
 
    void increment() {
        value++; // read, add, write
    }
}

Возможное чередование:

Поток AПоток Bvalue
читает 00
читает 00
пишет 11
пишет 11

Два вызова дали 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? Одной атомарной операцией или общей критической секцией.