Вопрос 19 · Раздел 9

Как предотвратить deadlock?

Deadlock можно предотвратить, нарушив хотя бы одно из 4 условий Коффмана. Ключевая идея: deadlock требует одновременного выполнения всех 4 условий, поэтому нарушение любого из н...

Версии по языкам: English Russian Ukrainian

Deadlock можно предотвратить, нарушив хотя бы одно из 4 условий Коффмана. Ключевая идея: deadlock требует одновременного выполнения всех 4 условий, поэтому нарушение любого из них делает deadlock невозможным.

В отличие от обнаружения deadlock (файл 19), который лишь диагностирует проблему, этот файл описывает превентивные стратегии — как спроектировать систему так, чтобы deadlock никогда не возник.


Junior уровень

Базовое понимание

Deadlock можно предотвратить, нарушив хотя бы одно из 4 условий Коффмана. Самый эффективный способ — Lock Ordering (упорядочивание блокировок).

Способ 1: Lock Ordering

Всегда захватывайте ресурсы в строго определённом порядке:

public class BankAccount {
    private final int id; // Уникальный ID для определения порядка
    private double balance;

    public BankAccount(int id, double balance) {
        this.id = id;
        this.balance = balance;
    }

    // Безопасный перевод денег — всегда в порядке ID
    public static void transfer(BankAccount from, BankAccount to, int amount) {
        // Определяем порядок по ID
        BankAccount first = from.id < to.id ? from : to;
        BankAccount second = from.id < to.id ? to : from;

        synchronized(first) {
            synchronized(second) {
                from.withdraw(amount);
                to.deposit(amount);
            }
        }
    }

    private void withdraw(int amount) { balance -= amount; }
    private void deposit(int amount) { balance += amount; }
}

Способ 2: tryLock с таймаутом

Используйте ReentrantLock.tryLock() вместо synchronized:

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

public void safeOperation() throws InterruptedException {
    while (true) {
        if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                    try {
                        // Оба захвачены — работаем
                        return;
                    } finally {
                        lock2.unlock();
                    }
                }
            } finally {
                lock1.unlock(); // Всегда освобождаем первый!
            }
        }
        // Не удалось — пауза и повтор
        Thread.sleep(50);
    }
}

Способ 3: Один глобальный лок

// Простой, но медленный способ
private final Object globalLock = new Object();

public void operation1() {
    synchronized(globalLock) {
        // Все операцииии через один лок — deadlock невозможен
    }
}

public void operation2() {
    synchronized(globalLock) {
        // Нет вложенных блокировок → нет deadlock
    }
}

Почему это медленно: все потоки выполняют операции последовательно, даже если операции не конфликтуют (например, operation1 читает данные, а operation2 пишет в другой файл). Это сводит многопоточность к однопоточности. Используйте только когда операции действительно должны быть строго последовательными.


Middle уровень

Lock Ordering с hashCode

Когда ресурсы не имеют натурального порядка:

public void safeOperation(Object resource1, Object resource2) {
    // Порядок по identity hash code
    int h1 = System.identityHashCode(resource1);
    int h2 = System.identityHashCode(resource2);

    if (h1 < h2) {
        synchronized(resource1) {
            synchronized(resource2) { /* ... */ }
        }
    } else if (h1 > h2) {
        synchronized(resource2) {
            synchronized(resource1) { /* ... */ }
        }
    } else {
        // Коллизия hashCode — используем глобальный tie-breaker
        synchronized(globalLock) {
            synchronized(resource1) {
                synchronized(resource2) { /* ... */ }
            }
        }
    }
}

Try-Lock с откатом

ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();

public boolean tryTransfer(Account from, Account to, int amount) {
    if (!lockA.tryLock()) return false;
    try {
        if (!lockB.tryLock()) return false; // Не удалось — отпускаем A
        try {
            from.withdraw(amount);
            to.deposit(amount);
            return true;
        } finally {
            lockB.unlock();
        }
    } finally {
        lockA.unlock();
    }
}

Это нарушает условие Hold and Wait — если не удалось получить второй ресурс, отпускаем первый.

Lock-free структуры данных

Лучший способ предотвратить deadlock — не использовать блокировки:

// Вместо synchronized HashMap:
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());

// Используйте ConcurrentHashMap — lock-free:
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();

// Вместо AtomicInteger + synchronized:
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // CAS — нет блокировки → нет deadlock

Сравнение подходов

Метод Плюсы Минусы Когда использовать
Lock Ordering Надёжный, простой Нужен порядок ресурсов Большинство случаев
Try-Lock Гибкий, с таймаутом Нужен retry-цикл Когда порядок неизвестен
Один лок Простой Низкий параллелизм Простые системы
Lock-free Нет deadlock вообще Сложнее реализовать Высокая нагрузка

Senior уровень

Under the Hood: Нарушение условий Коффмана

Метод Какое условие нарушает Механизм
Lock Ordering Circular Wait Невозможно создать цикл
Try-Lock + отпускание Hold and Wait Не держим при ожидании
lockInterruptibly No Preemption Можно прервать
Lock-free/Atomic Mutual Exclusion Нет эксклюзивного владения
Один глобальный лок Circular Wait Нет вложенности

Thread-Specific Resources (Шардирование)

Проектируйте систему так, чтобы потоки не делили ресурсы:

// Каждый поток работает со своим сегментом
public class ShardedCounter {
    private final Map<Integer, AtomicInteger> shards = new ConcurrentHashMap<>();

    public void increment(int userId) {
        int shard = userId % 16; // 16 сегментов
        shards.computeIfAbsent(shard, k -> new AtomicInteger(0))
              .incrementAndGet();
    }
}
// Потоки с разными userId никогда не конкурируют → нет deadlock

Верификация: Static Analysis

ThreadSanitizer (TSan)

# Для Java через LLVM
java -agentpath:/path/to/libtsan.so MyApp

Находит потенциальные гонки и deadlock ещё на этапе тестирования.

SonarQube

Обнаруживает вложенные synchronized блоки в разном порядке:

// SonarQube предупредит:
public void method1() {
    synchronized(lockA) {
        synchronized(lockB) { // ⚠️ Potential deadlock
        }
    }
}

public void method2() {
    synchronized(lockB) {
        synchronized(lockA) { // ⚠️ В обратном порядке!
        }
    }
}

Edge Cases

Deadlock с внешними системами

Поток Java: держит lock → ждёт ответ от БД
БД: держит lock на таблицу → ждёт данные от Java (через callback)

→ Deadlock между Java и БД!

Решение: не держите Java-блокировки во время I/O операций.

// ПЛОХО:
synchronized(lock) {
    database.update(); // Держим lock во время I/O
}

// ХОРОШО:
Object result = database.update(); // I/O без блокировки
synchronized(lock) {
    // Обновляем состояние
}

Interruption и synchronized

// synchronized НЕЛЬЗЯ прервать:
synchronized(lock) {
    // Если deadlock — поток будет висеть до перезагрузки JVM
}

// ReentrantLock МОЖНО прервать:
lock.lockInterruptibly();
try {
    // Если deadlock — можно вызвать thread.interrupt()
} finally {
    lock.unlock();
}

Диагностика и Мониторинг

ThreadMXBean

public class DeadlockMonitor {
    public static void checkAndAlert() {
        ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
        long[] deadlocked = mxBean.findDeadlockedThreads();

        if (deadlocked != null) {
            for (long threadId : deadlocked) {
                ThreadInfo info = mxBean.getThreadInfo(threadId, true, true);
                log.error("DEADLOCK: Thread {} blocked on {}",
                    info.getThreadName(),
                    info.getLockName());
            }
            // Send alert, restart, etc.
        }
    }
}

Warning: Это дорогая операция — не запускайте слишком часто (раз в 30+ секунд).

LockWaitTime метрики

// Мониторьте время захвата блокировок
long start = System.nanoTime();
lock.lock();
long waitTime = System.nanoTime() - start;

if (waitTime > 1_000_000) { // > 1ms
    log.warn("High lock wait time: {} ms", waitTime / 1_000_000);
}
// Внезапный рост = предвестник deadlock или contention

Best Practices

  1. Lock Ordering — самый надёжный способ, используйте всегда при вложенных блокировках
  2. tryLock(timeout) — для избежания вечного ожидания
  3. Избегайте блокировок при I/O — не держите lock во время запросов к БД/API
  4. Lock-free структуры — ConcurrentHashMap, Atomic* вместо synchronized
  5. ReentrantLock вместо synchronized — для прерываемости
  6. Шардирование — потоки с независимыми данными = нет deadlock
  7. Мониторьте LockWaitTime — рост = предвестник проблем
  8. Static Analysis — SonarQube найдёт вложенные блокировки на этапе разработки

Когда НЕ использовать prevention-стратегии

  • Одна блокировка, один поток — если у вас только один лок и один поток, deadlock невозможен по определению, не нужно усложнять
  • Только read-операции — если все потоки только читают данные (ReadLock или immutable), Mutual Exclusion не применяется, deadlock невозможен
  • Прототип/MVP на 1-2 дня — глобальный лок вполне подойдёт, рефакторите когда появится реальная нагрузка
  • Активная модель (Akka) — если вы уже используете акторы, deadlock prevention на уровне блокировок не применим — акторы не делят состояние

Lock Ordering vs Try-Lock: что выбрать?

Ситуация Выбор Почему
Фиксированный набор ресурсов (БД, файлы) Lock Ordering Порядок известен заранее, просто и надёжно
Динамические ресурсы (создаются на лету) Try-Lock Порядок неизвестен, tryLock гибче
Высокая нагрузка + известные ресурсы Lock Ordering Нулевой оверхед при захвате
Нужно избежать вечного ожидания Try-Lock + timeout Lock Ordering может ждать вечно при баге в порядке
Legacy-код с вложенными synchronized Try-Lock Переписать на порядок сложнее, чем заменить synchronized на tryLock

🎯 Шпаргалка для интервью

Обязательно знать:

  • Deadlock предотвращается нарушением хотя бы одного из 4 условий Коффмана
  • Lock Ordering — нарушение Circular Wait — самый надёжный и бесплатный способ
  • TryLock + отпускание при неударе — нарушение Hold and Wait
  • ReentrantLock вместо synchronized — нарушение No Preemption (поддержка interrupt)
  • Lock-free/Atomic — нарушение Mutual Exclusion
  • Не держите блокировки во время I/O операций — это приводит к deadlock с внешними системами (БД, API)

Частые уточняющие вопросы:

  • Что выбрать: Lock Ordering или Try-Lock? — Lock Ordering при фиксированном наборе ресурсов (нулевой оверхед), Try-Lock при динамических ресурсах или в legacy-коде
  • Как SonarQube помогает предотвратить deadlock? — Находит вложенные synchronized блоки в разном порядке на этапе разработки
  • Что такое шардирование как способ предотвращения? — Потоки работают с независимыми сегментами данных, нет конкуренции = нет deadlock

Красные флаги (НЕ говорить):

  • ❌ “Один глобальный лок — это хорошее решение для production” — убивает параллелизм, только для прототипов
  • ❌ “TryLock гарантирует отсутствие deadlock” — TryLock предотвращает вечное ожидание, но deadlock-паттерн всё ещё возможен при неправильной retry-логике
  • ❌ “Deadlock prevention не нужен для веб-приложений” — веб-приложения тоже используют БД, кэши, внешние API
  • ❌ “ReentrantLock решает все проблемы с deadlock” — он только даёт больше инструментов, deadlock всё ещё возможен при неправильном использовании

Связанные темы:

  • [[19. Какие условия необходимы для возникновения deadlock]]
  • [[21. Что такое race condition]]
  • [[22. Как избежать race condition]]
  • [[23. Что такое Virtual Threads в Java 21]]
  • [[27. В чём разница между Thread и Runnable]]