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

Что такое deadlock (взаимная блокировка)?

Два человека на узком мосту с разных сторон:

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

Junior уровень

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

Deadlock (взаимная блокировка) — это ситуация, когда два или более потока блокируют друг друга навсегда.

В коде: Поток 1 держит lockA и ждёт lockB. Поток 2 держит lockB и ждёт lockA. Ни один не отпустит свой lock первым. Оба потока зависли навсегда.

Простая аналогия

Два человека на узком мосту с разных сторон:

  • Человек A: “Я не пропущу тебя, пока ты не пропустишь меня”
  • Человек B: “Я не пропущу тебя, пока ты не пропустишь меня”
  • Результат: Оба стоят вечно

Пример в Java

public class DeadlockDemo {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void methodA() {
        synchronized(lock1) {
            System.out.println("Поток 1: Захватил lock1");
            // Пауза, чтобы другой поток успел захватить lock2
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            synchronized(lock2) { // ЖДЁМ lock2 — но он у другого потока!
                System.out.println("Поток 1: Захватил lock2");
            }
        }
    }

    public void methodB() {
        synchronized(lock2) {
            System.out.println("Поток 2: Захватил lock2");
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            synchronized(lock1) { // ЖДЁМ lock1 — но он у другого потока!
                System.out.println("Поток 2: Захватил lock1");
            }
        }
    }
}

// Запуск:
new Thread(demo::methodA).start();
new Thread(demo::methodB).start();
// → DEADLOCK! Поток 1 ждёт lock2, Поток 2 ждёт lock1

Deadlock vs Livelock vs Starvation

Тип Поведение CPU  
Deadlock Потоки “спят” и никогда не проснутся 0%  
Livelock Потоки постоянно реагируют друг на друга, но не делают прогресса 100%  
Starvation Поток готов к работе, но никогда не получает ресурс Зависит  

Livelock пример

// Два человека в коридоре — оба пытаются уступить, но в одну сторону
while (otherPersonMoving) {
    stepAside(); // Постоянно реагируем, но не проходим
}

Starvation пример

// Поток с низким приоритетом никогда не получает CPU
Thread lowPriority = new Thread(task);
lowPriority.setPriority(Thread.MIN_PRIORITY); // Всегда ждёт

Middle уровень

Wait-for-Graph (Граф ожидания)

В теории deadlock представляется как цикл в графе:

Поток 1 ──ждёт──→ Lock B ──держит──→ Поток 2
  ▲                                       │
  │                                       │
  └───────────ждёт──────── Lock A ◄───────┘
                держит

ЦИКЛ = DEADLOCK

Условия Коффмана (Coffman Conditions)

Deadlock возможен ТОЛЬКО при одновременном выполнении всех 4 условий:

# Условие Описание
1 Mutual Exclusion Ресурс может быть занят только одним потоком
2 Hold and Wait Поток держит один ресурс и ждёт другой
3 No Preemption (отсутствие принудительного изъятия) — если поток захватил synchronized(lock), никто не может насильно забрать у него этот lock. Только сам поток может выйти из synchronized.  
4 Circular Wait Существует замкнутая цепочка ожиданий

Database Deadlocks

Deadlock может возникнуть и на уровне БД:

// Поток 1:
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

// Поток 2:
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
// → DEADLOCK! БД обнаружит и откатит одну транзакцию

Отличие: БД умеет детектировать deadlock и автоматически откатывает одну транзакцию. В Java вам придётся детектировать вручную.

Hidden Deadlock (Скрытый)

// Deadlock через Thread Pool Starvation
ExecutorService pool = Executors.newFixedThreadPool(1);

// Пошаговый разбор:
// Шаг 1: pool.submit(A) — единственный поток начал выполнять задачу А
// Шаг 2: Внутри А вызывается pool.submit(B).get() — задача Б поставлена в очередь
// Шаг 3: .get() блокирует поток А в ожидании Б
// Шаг 4: Поток единственный и занят, Б никогда не начнётся = DEADLOCK
// Это называется "thread pool starvation deadlock"
Future<String> future = pool.submit(() -> {
    // Эта задача ждёт другую задачу из того же пула
    return pool.submit(() -> "result").get(); // DEADLOCK!
});
// Один поток занят первой задачей, вторая задача ждёт в очереди

Static Initialization Deadlock

// Класс A
class A {
    static {
        System.out.println(B.value); // Обращается к B
    }
}

// Класс B
class B {
    static {
        System.out.println(A.value); // Обращается к A
    }
}

// Два потока загружают A и B одновременно → deadlock в ClassLoader

Senior уровень

Когда НЕ использовать вложенные synchronized

  1. Порядок захвата нельзя гарантировать — разные пути кода захватывают locks в разном порядке
  2. Есть внешние вызовы внутри блокировки (БД, HTTP, файловая система) — внешний ресурс может заблокировать и создать deadlock
  3. Неизвестный код — передаёте lock в чужой метод, который может захватить другой lock

Альтернативы: ReentrantLock.tryLock(timeout), lock-free структуры (ConcurrentHashMap, AtomicReference), или один общий lock.

Under the Hood: JVM Deadlock Detection

JVM имеет встроенный детектор deadlock через ThreadMXBean:

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

if (deadlockedThreads != null) {
    for (long threadId : deadlockedThreads) {
        ThreadInfo info = mxBean.getThreadInfo(threadId, true, true);
        System.out.println("Deadlock: " + info.getThreadName());
        System.out.println("Blocked on: " + info.getLockName());
        System.out.println("Blocked by: " + info.getLockOwnerName());
    }
}

Важно: Это дорогая операция — не запускайте слишком часто.

Deadlock в Highload

Проблема: Deadlock может парализовать систему за миллисекунды
Детектор JVM: Работает периодически (не мгновенно)

Результат: потоки, которым нужны заблокированные ресурсы, зависнут навсегда. Остальная система продолжит работать, но пропускная способность упадёт.

Edge Cases

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

Java поток: держит lockA → ждёт ответ от БД
БД транзакция: держит lock на таблицу → ждёт HTTP от Java приложения

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

ReentrantLock и interruption

// synchronized нельзя прервать — поток будет висеть до перезагрузки
synchronized(lock) {
    // Deadlock здесь = вечное ожидание
}

// ReentrantLock можно прервать
lock.lockInterruptibly();
try {
    // Deadlock здесь = можно прервать через thread.interrupt()
} finally {
    lock.unlock();
}

Диагностика

jstack — самый быстрый способ

jstack -l <pid>

Вывод:

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007f... (object 0x000000076af0c8d0),
  which is held by "Thread-2"
"Thread-2":
  waiting to lock monitor 0x00007f... (object 0x000000076af0c8e0),
  which is held by "Thread-1"

VisualVM / JConsole

Кнопка “Check Deadlock” — мгновенная проверка.

Программная проверка

// Периодическая проверка (каждые 30 секунд)
ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor();
monitor.scheduleAtFixedRate(() -> {
    ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
    long[] deadlocked = mxBean.findDeadlockedThreads();
    if (deadlocked != null) {
        log.error("DEADLOCK DETECTED! Threads: {}", Arrays.toString(deadlocked));
        // Alert, restart, etc.
    }
}, 0, 30, TimeUnit.SECONDS);

Prevention Rule: Lock Ordering

Самый эффективный способ предотвращения — всегда захватывайте ресурсы в строго определённом порядке:

// Определяем порядок через hashCode
void transfer(Account from, Account to, int amount) {
    Object first = System.identityHashCode(from) < System.identityHashCode(to) ? from : to;
    Object second = first == from ? to : from;

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

Best Practices

  1. Lock Ordering — всегда захватывайте ресурсы в определённом порядке
  2. tryLock(timeout) — используйте ReentrantLock с таймаутом
  3. Избегайте вложенных блокировок — если возможно
  4. Lock-free структуры — ConcurrentHashMap, Atomic*
  5. Thread-specific resources — шардирование, независимые данные
  6. Мониторьте через ThreadMXBean.findDeadlockedThreads()
  7. jstack — первый инструмент при подозрении на deadlock
  8. Используйте ReentrantLock — вместо synchronized для прерываемости

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

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

  • Deadlock = два потока блокируют друг друга навсегда (Поток 1 держит lockA и ждёт lockB, Поток 2 держит lockB и ждёт lockA)
  • 4 условия Коффмана (все одновременно): Mutual Exclusion, Hold and Wait, No Preemption, Circular Wait
  • Deadlock vs Livelock vs Starvation: deadlock — потоки спят (0% CPU), livelock — реагируют но без прогресса (100% CPU), starvation — поток никогда не получает ресурс
  • Lock Ordering — главный способ предотвращения: всегда захватывайте ресурсы в строго определённом порядке (например, по hashCode)
  • ReentrantLock.tryLock(timeout) — позволяет избежать вечного ожидания, в отличие от synchronized
  • JVM детектирует deadlock через ThreadMXBean.findDeadlockedThreads(), но это дорогая операция
  • Скрытый deadlock: thread pool starvation — задача ждёт результат другой задачи из того же пула (особенно single-thread pool)
  • БД автоматически детектирует и откатывает deadlock, Java — нет (нужен ручной мониторинг)

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

  • Как предотвратить deadlock? — Lock ordering (фиксированный порядок захвата), tryLock(timeout), lock-free структуры, один общий lock
  • Чем synchronized отличается от ReentrantLock при deadlock? — synchronized нельзя прервать (вечное ожидание), ReentrantLock.lockInterruptibly() можно прервать через thread.interrupt()
  • Что такое thread pool starvation deadlock? — Задача в пуле ждёт .get() от другой задачи в том же пуле; если пул single-thread — deadlock гарантирован
  • Как обнаружить deadlock в production? — jstack -l , ThreadMXBean.findDeadlockedThreads(), VisualVM «Check Deadlock»

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

  • “Deadlock невозможен в моём коде” — он часто скрытый (вложенные synchronized, внешние вызовы)
  • “JVM автоматически разрешает deadlock” — нет, JVM только детектирует, но не разрешает (в отличие от БД)
  • “shutdownNow() решит deadlock” — нет, он посылает interrupt(), но synchronized нельзя прервать
  • “Достаточно одного lock для предотвращения” — нет, deadlock возникает при 2+ ресурсах в разном порядке

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

  • [[10. Как работают AtomicInteger, AtomicLong]]
  • [[11. В чём преимущество Atomic классов перед synchronized]]
  • [[12. Что такое пул потоков (Thread Pool)]]
  • [[15. Что делает ExecutorService]]