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

Как бороться с состоянием гонки (Race condition) в многопоточности

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

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

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

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

Как бороться с Race Condition в многопоточности

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

Суть проблемы

public class BankAccount {
    private int balance = 100;  // Начальный баланс
    
    public void withdraw(int amount) {
        // Это НЕ атомарная операция!
        if (balance >= amount) {              // Шаг 1
            balance = balance - amount;       // Шаг 2
        }
    }
    
    public int getBalance() {
        return balance;
    }
}

// Сценарий race condition:
BankAccount account = new BankAccount();  // balance = 100

// Два потока одновременно пытаются снять по 60
Thread thread1 = new Thread(() -> account.withdraw(60));
Thread thread2 = new Thread(() -> account.withdraw(60));

thread1.start();
thread2.start();

thread1.join();
thread2.join();

System.out.println(account.getBalance());
// Ожидаемый результат: -20 (снять 120 из 100)
// На самом деле: может быть -20, может быть 40!

Что происходит:

Поток 1: прочитал balance = 100
Поток 2: прочитал balance = 100 (потому что Поток 1 ещё не записал)

Поток 1: 100 - 60 = 40, записал balance = 40
Поток 2: 100 - 60 = 40, записал balance = 40

ИТОГ: balance = 40 (вместо -20)

Одно снятие было потеряно!

Решение 1: Synchronized (синхронизация)

Самый простой способ — сделать операцию атомарной

public class BankAccount {
    private int balance = 100;
    
    // synchronized гарантирует, что только один поток может быть внутри
    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            balance = balance - amount;
        }
    }
    
    public synchronized int getBalance() {
        return balance;
    }
}

// Теперь результат всегда предсказуемый: -20

Как это работает:

Поток 1: начинает synchronized блок, берёт монитор (lock)
Поток 2: ждёт, пока Поток 1 отпустит монитор

Поток 1: атомарно (безопасно) выполнил операцию
Поток 1: отпустил монитор

Поток 2: теперь может взять монитор и выполнить

РЕЗУЛЬТАТ: операции выполнились последовательно

Проблема: deadlock (взаимная блокировка)

public class Account {
    private int balance;
    
    // ❌ Опасно: может быть deadlock
    public synchronized void transfer(Account other, int amount) {
        if (this.balance >= amount) {
            this.balance -= amount;
            other.withdraw(amount);  // Если other.withdraw() тоже synchronized
                                     // и другой поток уже заблокировал other
                                     // Deadlock!
        }
    }
    
    public synchronized void withdraw(int amount) {
        this.balance -= amount;
    }
}

Решение 2: ReentrantLock (явная блокировка)

Больше контроля, но требует аккуратности

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BankAccount {
    private int balance = 100;
    private final Lock lock = new ReentrantLock();
    
    public void withdraw(int amount) {
        lock.lock();
        try {
            if (balance >= amount) {
                balance = balance - amount;
            }
        } finally {
            lock.unlock();  // КРИТИЧНО: unlock в finally!
        }
    }
    
    public int getBalance() {
        lock.lock();
        try {
            return balance;
        } finally {
            lock.unlock();
        }
    }
}

// С tryLock (попробовать, не ждать):
public boolean withdrawNonBlocking(int amount, long timeoutMillis) {
    try {
        if (lock.tryLock(timeoutMillis, TimeUnit.MILLISECONDS)) {
            try {
                if (balance >= amount) {
                    balance -= amount;
                    return true;
                }
            } finally {
                lock.unlock();
            }
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    return false;
}

Решение 3: Volatile (для простых переменных)

Гарантирует видимость изменений между потоками

public class Flag {
    private volatile boolean running = true;  // Видно всем потокам
    
    public void stop() {
        running = false;  // Все потоки сразу увидят
    }
    
    public boolean isRunning() {
        return running;
    }
}

// Использование:
Flag flag = new Flag();
Thread worker = new Thread(() -> {
    while (flag.isRunning()) {  // Всегда читает свежее значение
        doWork();
    }
});
worker.start();

Thread.sleep(1000);
flag.stop();  // Сразу же worker заметит

❌ Volatile НЕ защищает от race condition!

private volatile int counter = 0;

public void increment() {
    counter++;  // ❌ Это НЕ атомарно!
    // 1. Прочитать counter
    // 2. Увеличить
    // 3. Написать counter
}

// Два потока могут прочитать одно и то же значение
// Использовать AtomicInteger вместо этого!

Решение 4: AtomicInteger / AtomicLong / AtomicReference

Атомарные операции без явной синхронизации

import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet();  // Атомарно!
    }
    
    public int getValue() {
        return count.get();
    }
    
    // Compare-and-swap операция
    public boolean compareAndSet(int expected, int newValue) {
        return count.compareAndSet(expected, newValue);
    }
}

// Использование
Counter counter = new Counter();
for (int i = 0; i < 1000; i++) {
    new Thread(counter::increment).start();
}

// Результат всегда 1000 (благодаря AtomicInteger)

Решение 5: ConcurrentHashMap / ConcurrentLinkedQueue

Thread-safe коллекции

import java.util.concurrent.ConcurrentHashMap;

public class UserCache {
    // ❌ Не безопасно для параллельного доступа
    // private Map<Integer, User> cache = new HashMap<>();
    
    // ✅ Безопасно: используется внутренняя синхронизация
    private ConcurrentHashMap<Integer, User> cache = 
        new ConcurrentHashMap<>();
    
    public void put(Integer id, User user) {
        cache.put(id, user);
    }
    
    public User get(Integer id) {
        return cache.get(id);
    }
    
    // Атомарные операции
    public User putIfAbsent(Integer id, User user) {
        return cache.putIfAbsent(id, user);
    }
}

// ConcurrentLinkedQueue для очередей
ConcurrentLinkedQueue<Task> queue = new ConcurrentLinkedQueue<>();
new Thread(() -> {
    while (true) {
        Task task = queue.poll();
        if (task != null) {
            processTask(task);
        }
    }
}).start();

new Thread(() -> {
    queue.offer(new Task(...));  // Безопасно добавлять из другого потока
}).start();

Решение 6: ReadWriteLock

Множество читателей, но один писатель

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class SharedResource {
    private String data = "initial";
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    // Много потоков могут одновременно читать
    public String read() {
        lock.readLock().lock();
        try {
            return data;
        } finally {
            lock.readLock().unlock();
        }
    }
    
    // Но только один может писать (и блокирует всех читателей)
    public void write(String newData) {
        lock.writeLock().lock();
        try {
            data = newData;
        } finally {
            lock.writeLock().unlock();
        }
    }
}

Правила хорошей практики

  1. Минимизируй критические секции: держи только нужное в synchronized
  2. Избегай вложенных блокировок: если возможно, не вызывай другие synchronized методы
  3. Используй высокоуровневые API: ConcurrentHashMap вместо Collections.synchronizedMap()
  4. Атомарные классы для простых переменных: AtomicInteger вместо volatile
  5. Неизменяемость — лучшее решение: если данные не меняются, нет race condition
  6. Тестируй с несколькими потоками: используй stress tests

Итого

Бороться с race condition можно:

  1. synchronized — простое, но может быть медленным
  2. ReentrantLock — больше контроля
  3. volatile — только для видимости, не для atomicity
  4. AtomicInteger/Long — для простых операций
  5. Thread-safe коллекции — ConcurrentHashMap, ConcurrentLinkedQueue
  6. ReadWriteLock — когда много читателей, мало писателей
  7. Неизменяемость — лучший подход, если применим