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

Как можно избежать deadlock?

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

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

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

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

Избегание deadlock-ов в многопоточных приложениях

Dead lock — один из самых коварных багов. За 10+ лет видел проекты, которые падали в production из-за deadlock-а. Вот проверенные техники.

Что такое deadlock

Классический пример:

Тред 1: Захватил Lock A, хочет захватить Lock B
Тред 2: Захватил Lock B, хочет захватить Lock A

Оба ждут друг друга... DEADLOCK!
public class DeadlockExample {
  private final Object lock1 = new Object();
  private final Object lock2 = new Object();
  
  public void method1() {
    synchronized (lock1) {
      System.out.println("Thread 1: Захватил lock1");
      try { Thread.sleep(100); } catch (InterruptedException e) {}
      
      synchronized (lock2) { // Ждёт lock2
        System.out.println("Thread 1: Захватил lock2");
      }
    }
  }
  
  public void method2() {
    synchronized (lock2) { // Захватил lock2 первым
      System.out.println("Thread 2: Захватил lock2");
      try { Thread.sleep(100); } catch (InterruptedException e) {}
      
      synchronized (lock1) { // Ждёт lock1 - DEADLOCK!
        System.out.println("Thread 2: Захватил lock1");
      }
    }
  }
}

Четыре условия deadlock-а (Coffman conditions)

Для deadlock-а нужны ВСЕ 4 условия одновременно:

  1. Mutual Exclusion — ресурс может использовать только один тред
  2. Hold and Wait — тред держит ресурс и ждёт другой
  3. No Preemption — ресурс нельзя отобрать (только добровольно отпустить)
  4. Circular Wait — циклическая цепочка ожидания

Для избегания достаточно устранить ЛЮБОЕ из них.

1. Избегать вложенных synchronization блоков

❌ Плохо: вложенные блокировки

public void processPayment(Account from, Account to) {
  synchronized (from) { // Lock на счёте 1
    synchronized (to) {  // Lock на счёте 2
      transfer(from, to); // Potential deadlock
    }
  }
}

✅ Хорошо: упорядочить блокировки

public void processPayment(Account from, Account to) {
  // Всегда блокируем в одинаковом порядке!
  Account first = from.getId() < to.getId() ? from : to;
  Account second = from.getId() < to.getId() ? to : from;
  
  synchronized (first) {
    synchronized (second) {
      transfer(from, to); // No deadlock - всегда одинаковый порядок
    }
  }
}

2. Использовать ReentrantLock с timeout

Самая надёжная техника — timeout при захвате lock-а.

private final ReentrantLock lock1 = new ReentrantLock();
private final ReentrantLock lock2 = new ReentrantLock();

public void transfer(Account from, Account to) throws InterruptedException {
  // Пытаемся захватить с timeout
  if (!lock1.tryLock(1, TimeUnit.SECONDS)) {
    throw new TimeoutException("Не смог захватить lock1");
  }
  
  try {
    if (!lock2.tryLock(1, TimeUnit.SECONDS)) {
      throw new TimeoutException("Не смог захватить lock2");
    }
    
    try {
      // Работаем
      transfer(from, to);
    } finally {
      lock2.unlock();
    }
  } finally {
    lock1.unlock();
  }
}

Как это помогает:

  • Если timeout истёк → освобождаем текущий lock
  • Другой тред может продолжить работу
  • Deadlock физически невозможен!

3. Использовать concurrent коллекции вместо synchronized

❌ Плохо: ручная синхронизация

private Map<String, Integer> map = new HashMap<>();

public void update(String key, int value) {
  synchronized (map) {
    int current = map.get(key);
    map.put(key, current + value);
  }
}

✅ Хорошо: ConcurrentHashMap

private Map<String, Integer> map = new ConcurrentHashMap<>();

public void update(String key, int value) {
  map.compute(key, (k, v) -> (v == null) ? value : v + value);
  // Внутри используются более тонкие блокировки (segment locking)
  // Deadlock минимален
}

4. Использовать immutable объекты

Если объект immutable → нет нужды синхронизировать.

// ❌ Плохо: mutable, нужна синхронизация
public class Account {
  private int balance;
  
  public synchronized void transfer(Account to, int amount) {
    this.balance -= amount;
    to.balance += amount; // Potential deadlock
  }
}

// ✅ Хорошо: immutable
public final class Account {
  private final int balance;
  private final String id;
  
  public Account transfer(int amount) {
    return new Account(balance - amount, id); // Новый объект
  }
}

// Использование
Account acc1 = new Account(1000, "1");
Account acc2 = new Account(500, "2");

Account newAcc1 = acc1.transfer(100);
Account newAcc2 = acc2.transfer(-100);
// Никаких deadlock-ов!

5. Избегать synchronized на слишком крупное время

Блокируй только критическую секцию.

// ❌ Плохо: долгая блокировка
public synchronized void processOrder(Order order) {
  updateDatabase(order);      // Может быть медленно
  sendEmail(order);           // Может быть медленно
  logToAnalytics(order);      // Может быть медленно
}

// ✅ Хорошо: блокируем только необходимое
public void processOrder(Order order) {
  Order validOrder;
  synchronized (orders) {
    validOrder = orders.get(order.getId()); // Критическая секция
  }
  
  // Вне блокировки
  updateDatabase(validOrder);
  sendEmail(validOrder);
  logToAnalytics(validOrder);
}

6. Использовать ReadWriteLock для читающих операций

private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int value = 0;

public int getValue() {
  lock.readLock().lock(); // Много потоков могут одновременно читать
  try {
    return value;
  } finally {
    lock.readLock().unlock();
  }
}

public void setValue(int newValue) {
  lock.writeLock().lock(); // Только один тред может писать
  try {
    value = newValue;
  } finally {
    lock.writeLock().unlock();
  }
}

7. Использовать Executors для управления потоками

// ❌ Плохо: создаём потоки вручную, сложнее управлять
Thread t1 = new Thread(() -> { ... });
Thread t2 = new Thread(() -> { ... });
t1.start();
t2.start();

// ✅ Хорошо: ExecutorService контролирует потоки и ресурсы
ExecutorService executor = Executors.newFixedThreadPool(10);
try {
  executor.submit(() -> { ... });
  executor.submit(() -> { ... });
} finally {
  executor.shutdown();
}

8. Обнаружение deadlock-а в production

ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();

if (deadlockedThreads != null) {
  System.err.println("DEADLOCK DETECTED!");
  ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads);
  for (ThreadInfo info : threadInfos) {
    System.err.println(info);
  }
}

Чеклист избегания deadlock-ов

✅ Всегда захватывай locks в одинаковом порядке ✅ Используй tryLock() с timeout вместо synchronized ✅ Используй concurrent коллекции (ConcurrentHashMap, CopyOnWriteArrayList) ✅ Минимизируй время в synchronized блоках ✅ Избегай nested synchronized ✅ Используй ReadWriteLock для read-heavy операций ✅ Делай объекты immutable где возможно ✅ Тестируй многопоточность под нагрузкой

Практический пример: Безопасный transfer

public class SafeBank {
  private final Map<String, ReentrantLock> accountLocks = new ConcurrentHashMap<>();
  private final Map<String, Integer> accounts = new ConcurrentHashMap<>();
  
  public boolean transfer(String from, String to, int amount) throws InterruptedException {
    ReentrantLock lockFrom = accountLocks.computeIfAbsent(from, k -> new ReentrantLock());
    ReentrantLock lockTo = accountLocks.computeIfAbsent(to, k -> new ReentrantLock());
    
    // Всегда в одинаковом порядке (по имени)
    ReentrantLock first = from.compareTo(to) < 0 ? lockFrom : lockTo;
    ReentrantLock second = from.compareTo(to) < 0 ? lockTo : lockFrom;
    
    // С timeout
    if (!first.tryLock(2, TimeUnit.SECONDS)) return false;
    try {
      if (!second.tryLock(2, TimeUnit.SECONDS)) return false;
      try {
        if (accounts.get(from) >= amount) {
          accounts.put(from, accounts.get(from) - amount);
          accounts.put(to, accounts.get(to) + amount);
          return true;
        }
        return false;
      } finally {
        second.unlock();
      }
    } finally {
      first.unlock();
    }
  }
}

Этот код физически не может deadlock-ить.