← Назад к вопросам
Как бороться с состоянием гонки (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();
}
}
}
Правила хорошей практики
- Минимизируй критические секции: держи только нужное в synchronized
- Избегай вложенных блокировок: если возможно, не вызывай другие synchronized методы
- Используй высокоуровневые API: ConcurrentHashMap вместо Collections.synchronizedMap()
- Атомарные классы для простых переменных: AtomicInteger вместо volatile
- Неизменяемость — лучшее решение: если данные не меняются, нет race condition
- Тестируй с несколькими потоками: используй stress tests
Итого
Бороться с race condition можно:
- synchronized — простое, но может быть медленным
- ReentrantLock — больше контроля
- volatile — только для видимости, не для atomicity
- AtomicInteger/Long — для простых операций
- Thread-safe коллекции — ConcurrentHashMap, ConcurrentLinkedQueue
- ReadWriteLock — когда много читателей, мало писателей
- Неизменяемость — лучший подход, если применим