← Назад к вопросам

Какие знаешь проблемы, которые возникают из-за Race condition?

1.8 Middle🔥 231 комментариев
#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Проблемы, которые возникают из-за Race Condition

Race condition — это один из наиболее коварных и сложно воспроизводимых багов в многопоточном коде. Это происходит, когда два или более потока обращаются к общему ресурсу одновременно и хотя бы один из них его модифицирует.

1. Lost Updates (Потерянные обновления)

Двоим потокам одновременно нужно увеличить счетчик:

private int counter = 0;

public void increment() {
    counter++;  // Это НЕ атомарная операция!
}

// Поток 1: читает counter (0) → увеличивает (1) → пишет (1)
// Поток 2: читает counter (0) → увеличивает (1) → пишет (1)
// Результат: 1 вместо 2. Одно увеличение потеряно.

Проблема: из-за отсутствия синхронизации оба потока прочитали одно и то же значение.

Решение: использовать synchronized, AtomicInteger или ReentrantLock:

public synchronized void increment() {
    counter++;
}

// Или
private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
    counter.incrementAndGet();  // Атомарно
}

2. Dirty Reads (Грязные чтения)

private volatile boolean flag = false;
private int value = 0;

public void write() {
    value = 42;
    flag = true;  // Сигнал, что значение готово
}

public int read() {
    if (flag) {
        return value;  // Можно ли вернуть value, если флаг = true?
    }
    return -1;
}

Проблема: без volatile поток может прочитать старое значение value, хотя flag = true. Видимость памяти не гарантирована.

Решение: использовать volatile для видимости памяти между потоками:

private volatile boolean flag = false;
private volatile int value = 0;

3. Check-then-act race condition

Одна из самых коварных проблем:

public class LazyInitialization {
    private static HashMap<String, String> map = null;
    
    public static synchronized Map<String, String> getMap() {
        if (map == null) {  // Check
            map = new HashMap<>();  // Act (инициализация)
        }
        return map;
    }
}

// Проблема БЕЗ synchronized:
// Поток 1: проверил (map == null) → true
// Поток 2: проверил (map == null) → true (потому что 1 еще не инициализировал)
// Оба создают HashMap, теряется один из них

Решение: двойная проверка блокировки или eager initialization:

// Double-checked locking
public static Map<String, String> getMap() {
    if (map == null) {
        synchronized (LazyInitialization.class) {
            if (map == null) {
                map = new HashMap<>();
            }
        }
    }
    return map;
}

// Лучше: eager initialization
private static final Map<String, String> map = new HashMap<>();
public static Map<String, String> getMap() {
    return map;
}

4. Использование некопируемых объектов

public class Counter {
    private int count = 0;
    
    public void increment() {
        count++;
    }
    
    public int getCount() {
        return count;
    }
}

Counter counter = new Counter();

// Поток 1
new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        counter.increment();
    }
}).start();

// Поток 2
new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        counter.increment();
    }
}).start();

// Ожидаемый результат: 2000
// Реальный результат: может быть 1000-2000

Решение:

public class ThreadSafeCounter {
    private final AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet();
    }
    
    public int getCount() {
        return count.get();
    }
}

5. Inconsistent state (Несогласованное состояние)

public class BankAccount {
    private long balance;
    private int version;
    
    public synchronized void transfer(long amount) {
        balance -= amount;
        // Если исключение здесь, то balance изменился, но version нет
        version++;
    }
    
    public synchronized void rollback() {
        // Как откатить? У нас нет предыдущего состояния
    }
}

Проблема: объект переходит в непредсказуемое состояние.

Решение: используй транзакции, locking и careful error handling:

public synchronized void transfer(long amount) {
    long oldBalance = balance;
    try {
        balance -= amount;
        version++;
    } catch (Exception e) {
        balance = oldBalance;
        throw e;
    }
}

6. Deadlock (Взаимная блокировка)

public class Account {
    private long balance;
    
    public synchronized void transferTo(Account target, long amount) {
        balance -= amount;
        target.balance += amount;  // Вызов synchronized метода
    }
}

Account acc1 = new Account(100);
Account acc2 = new Account(100);

// Поток 1: acc1.transferTo(acc2, 50)
// Поток 2: acc2.transferTo(acc1, 50)
// DEADLOCK! Оба потока ждут друг друга.

Решение: упорядочить захват блокировок:

public synchronized void transferTo(Account target, long amount) {
    Account first, second;
    if (this.id < target.id) {
        first = this; second = target;
    } else {
        first = target; second = this;
    }
    
    synchronized (first) {
        synchronized (second) {
            this.balance -= amount;
            target.balance += amount;
        }
    }
}

7. Memory visibility issues (Видимость памяти)

public class Flag {
    private boolean shouldStop = false;  // БЕЗ volatile
    
    public void stop() {
        shouldStop = true;
    }
    
    public void run() {
        while (!shouldStop) {
            // Бесконечный цикл!
            // Поток может не увидеть обновление shouldStop
            // из другого потока
        }
    }
}

Решение: использовать volatile:

private volatile boolean shouldStop = false;

8. ABA Problem

public class Stack {
    private Node head = null;
    
    public void push(int value) {
        head = new Node(value, head);
    }
    
    public int pop() {
        Node h = head;
        head = h.next;  // Race condition!
        return h.value;
    }
}

// Поток 1: читает head (A)
// Поток 2: pop() → head становится B
// Поток 2: pop() → head становится A (тот же объект!)
// Поток 1: думает, что state не изменился, но это не так

Решение: использовать stamped/versioned references:

private AtomicReference<VersionedHead> head = 
    new AtomicReference<>(new VersionedHead(null, 0));

9. Феномены многопоточности

  • Stale reads: чтение старых данных
  • Lost updates: потеря обновлений
  • Out-of-order execution: переорганизация команд CPU
  • Cache coherency issues: разные кэши на разных ядрах

10. Как избежать race conditions

  1. Immutability: сделать объект неизменяемым
  2. ThreadLocal: дать каждому потоку свою копию
  3. Synchronized: синхронизировать доступ
  4. Atomic classes: использовать AtomicInteger, AtomicReference
  5. Locks: ReentrantLock, ReadWriteLock
  6. Concurrent collections: ConcurrentHashMap, CopyOnWriteArrayList
  7. Volatile: для простых флагов видимости
  8. Message passing: вместо shared memory (actors, channels)
  9. Testing: использовать ThreadSanitizer, jcstress

Стратегия тестирования

// jcstress тест для race conditions
@JCStressTest
public static class RaceConditionTest {
    private int counter = 0;
    
    @Actor
    public void actor1() {
        counter++;
    }
    
    @Actor
    public void actor2() {
        counter++;
    }
    
    @Arbiter
    public void arbiter(II_Result r) {
        r.r1 = counter;
    }
}
Какие знаешь проблемы, которые возникают из-за Race condition? | PrepBro