JavaRush /Курсы /JAVA 25 SELF /Deadlock: причины, примеры, устранение

Deadlock: причины, примеры, устранение

JAVA 25 SELF
53 уровень , 0 лекция
Открыта

1. Углубляемся в Deadlock

Deadlock («взаимная блокировка») — это ситуация, когда два или более потоков ждут друг друга вечно: каждый удерживает какой-то ресурс и пытается получить другой, уже занятый соседним потоком. В итоге никто не может продолжить работу, и программа «зависает». Это как две машины на узком мосту нос к носу: пока одна не сдаст назад — никто не проедет.

В Java deadlock — это не исключение (Exception), а «зависание» программы. Потоки не завершаются и не падают с ошибкой, а просто ждут друг друга до бесконечности. Поэтому такие ошибки коварны: они проявляются только «при определённых обстоятельствах».

Четыре условия возникновения deadlock

  • 1. Взаимное исключение (Mutual Exclusion)
    Ресурс может быть захвачен только одним потоком одновременно (например, объект-монитор, файл).
  • 2. Удержание и ожидание (Hold and Wait)
    Поток удерживает один ресурс и пытается получить второй, не отпуская первый.
  • 3. Неотъемлемость (No Preemption)
    Ресурс нельзя «отобрать» у потока: только сам поток может его освободить.
  • 4. Круговое ожидание (Circular Wait)
    Есть замкнутая цепочка, где каждый поток ждёт ресурс, удерживаемый следующим в цепи.

Deadlock возможен только если выполняются ВСЕ эти условия одновременно. Достаточно нарушить хотя бы одно — и взаимная блокировка невозможна.

2. Пример кода с deadlock

Схема

  • Есть два ресурса: lock1 и lock2 (обычные объекты).
  • Поток A сначала захватывает lock1, затем пытается получить lock2.
  • Поток B сначала захватывает lock2, затем пытается получить lock1.

Пример (не делайте так у себя дома!)

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

    public static void main(String[] args) {
        // Поток 1
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Поток 1: захватил lock1");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                System.out.println("Поток 1: пытается захватить lock2");
                synchronized (lock2) {
                    System.out.println("Поток 1: захватил lock2");
                }
            }
        });

        // Поток 2
        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Поток 2: захватил lock2");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                System.out.println("Поток 2: пытается захватить lock1");
                synchronized (lock1) {
                    System.out.println("Поток 2: захватил lock1");
                }
            }
        });

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

Что произойдёт при запуске?

  • Поток 1 захватывает lock1, поток 2 — lock2.
  • Оба «засыпают» на 100 миллисекунд, успевая захватить первый замок.
  • Затем каждый пытается захватить второй замок, который уже удерживается другим потоком.
  • Оба потока ждут друг друга бесконечно — получился deadlock.

В консоли увидите:

Поток 1: захватил lock1
Поток 2: захватил lock2
Поток 1: пытается захватить lock2
Поток 2: пытается захватить lock1

Почему возникает deadlock? Подробный разбор

  • Взаимное исключение: каждый замок может удерживаться только одним потоком.
  • Удержание и ожидание: Поток 1 держит lock1 и ждёт lock2; Поток 2 держит lock2 и ждёт lock1.
  • Неотъемлемость: никто не может «выдернуть» замок извне — только выйти из блока synchronized.
  • Круговое ожидание: ожидание замкнулось по кругу между двумя потоками.

3. Как избежать и предотвратить deadlock

Захват ресурсов в одном и том же порядке

Главное правило: если несколько потоков должны захватывать несколько ресурсов — всегда делайте это в одном и том же порядке. Например: сначала lock1, потом lock2 — для всех мест в коде.

public void doSomething() {
    Object firstLock = lock1;
    Object secondLock = lock2; // единый порядок "lock1 -> lock2"
    synchronized (firstLock) {
        synchronized (secondLock) {
            // Работа с обоими ресурсами
        }
    }
}

При едином порядке невозможно круговое ожидание — условие deadlock нарушается.

Использование tryLock с таймаутом (ReentrantLock)

Когда заранее неизвестно, какие ресурсы понадобятся, используйте ReentrantLock и метод tryLock, чтобы не ждать бесконечно.

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

public class TryLockDemo {
    private static final ReentrantLock lock1 = new ReentrantLock();
    private static final ReentrantLock lock2 = new ReentrantLock();

    public void doWork() {
        try {
            if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
                try {
                    if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                        try {
                            // Критическая секция
                        } finally {
                            lock2.unlock();
                        }
                    } else {
                        System.out.println("Не удалось захватить lock2, откатываемся");
                    }
                } finally {
                    lock1.unlock();
                }
            } else {
                System.out.println("Не удалось захватить lock1, откатываемся");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Плюс: можно откатить операцию и попробовать позже. Минус: код сложнее, но deadlock исключён.

Минимизируйте время удержания блокировок

Держите замок как можно меньше. Делайте внутри synchronized/lock только необходимое; всё остальное выносите за пределы критической секции.

Избегайте вложенных блокировок

Чем меньше вложенных synchronized/lock, тем ниже риск взаимной блокировки. Если можно — используйте один замок для связанных ресурсов.

Используйте готовые thread-safe структуры

Стандартная библиотека уже решила многие задачи: коллекции вроде ConcurrentHashMap и другие классы из java.util.concurrent минимизируют риск deadlock и упрощают код.

4. Диагностика deadlock

Thread Dump и jstack

Thread Dump — снимок состояния всех потоков в JVM.

Получение из консоли:

jstack <pid>

Где <pid> — идентификатор Java-процесса (можно узнать через jps).

В IDE обычно есть кнопка «Thread Dump» в панели отладки.

В дампе ищите:

  • Статусы BLOCKED или WAITING.
  • Сообщения вида waiting to lock ... и locked ....
  • Фразу JVM: "Found one Java-level deadlock:" — когда взаимная блокировка обнаружена.

Пример куска дампа:

"Thread-1":
  waiting to lock monitor 0x000000001e4000, (object 0x7f8a5c00, a java.lang.Object),
  which is held by "Thread-2"
"Thread-2":
  waiting to lock monitor 0x000000001e3000, (object 0x7f8a5c10, a java.lang.Object),
  which is held by "Thread-1"

Это и есть deadlock: потоки «держат» друг друга.

VisualVM и другие инструменты

  • VisualVM — бесплатный инструмент анализа потоков и обнаружения deadlock.
  • Java Mission Control / Flight Recorder — продвинутый мониторинг и профилирование.

В VisualVM можно посмотреть дерево потоков, их состояния и получить уведомление о взаимной блокировке.

Таблица: «Как не попасть в deadlock»

Причина deadlock Как избежать
Захват ресурсов в разном порядке Всегда придерживаться одного порядка захвата ресурсов
Вложенные synchronized Минимизировать вложенность, по возможности использовать один замок
Долгое удержание замка Сократить работу внутри критической секции
Использование нескольких замков одновременно Применять tryLock с таймаутом и уметь откатываться
Не thread-safe коллекции Использовать конкурентные коллекции (ConcurrentHashMap и др.)

5. Типичные ошибки при работе с deadlock

Ошибка №1: Захват ресурсов в разном порядке. Самая частая причина — разные потоки берут замки в разном порядке. Даже если «так быстрее», придерживайтесь единого порядка — иначе круговое ожидание гарантировано.

Ошибка №2: Вложенные synchronized без необходимости. Лишняя вложенность повышает риск взаимной блокировки и усложняет код. Упрощайте модель блокировок.

Ошибка №3: Игнорирование tryLock и таймаутов. Блокировка через synchronized заставит поток ждать бесконечно. Если нет уверенности, используйте tryLock с таймаутом и логику отката.

Ошибка №4: Долгое выполнение кода внутри блокировки. Сетевые запросы, I/O и тяжёлые вычисления под замком резко повышают вероятность проблем. Выносите их за пределы критической секции.

Ошибка №5: Не анализировать Thread Dump. Thread Dump — ваш лучший друг при поиске взаимных блокировок. Используйте jstack и анализируйте состояния BLOCKED/WAITING, цепочки «holding/waiting to lock».

1
Задача
JAVA 25 SELF, 53 уровень, 0 лекция
Недоступна
Передача эстафетной палочки между роботами: Опасность тупика 🤖
Передача эстафетной палочки между роботами: Опасность тупика 🤖
1
Задача
JAVA 25 SELF, 53 уровень, 0 лекция
Недоступна
Спасение от тупика: Умные инженеры с таймаутом 🛠️
Спасение от тупика: Умные инженеры с таймаутом 🛠️
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Andrey Уровень 1
24 октября 2025
53+
I'll kick them all Уровень 5
14 октября 2025
53