Каковы основные причины зависания (deadlock) программы на Java?

«Каковы основные причины зависания (deadlock) программы на Java?» — вопрос из категории Other, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Зависание программы часто вызвано взаимной блокировкой (deadlock), когда два или более потока бесконечно ожидают ресурсы, захваченные друг другом. Для возникновения deadlock необходимо одновременное выполнение четырех условий (условия Коффмана):

  1. Взаимное исключение: ресурс не может быть использован более чем одним потоком одновременно.
  2. Удержание и ожидание: поток удерживает один ресурс и ожидает доступ к другому.
  3. Отсутствие вытеснения: ресурс может быть освобожден только добровольно удерживающим его потоком.
  4. Циклическое ожидание: возникает кольцевая цепочка потоков, каждый из которых ждет ресурс, удерживаемый следующим в цепочке.

Типичный пример deadlock на Java:

final Object lock1 = new Object();
final Object lock2 = new Object();

new Thread(() -> {
    synchronized (lock1) {
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        synchronized (lock2) { // Поток будет ждать, пока lock2 не освободится
            System.out.println("Thread 1 acquired both locks");
        }
    }
}).start();

new Thread(() -> {
    synchronized (lock2) {
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        synchronized (lock1) { // Поток будет ждать, пока lock1 не освободится
            System.out.println("Thread 2 acquired both locks");
        }
    }
}).start();
// Оба потока зависнут.

Стратегии предотвращения и решения:

  • Упорядочивание блокировок: всегда захватывать несколько блокировок в одном и том же глобальном порядке (например, сначала lock1, потом lock2).
  • Использование tryLock из java.util.concurrent.locks.ReentrantLock с таймаутом.
  • Анализ дампов потоков с помощью jstack <PID> или профилировщиков (VisualVM, JProfiler) для выявления циклов ожидания.
  • Проектирование без избыточной синхронизации: использование потокобезопасных коллекций из java.util.concurrent и атомарных операций.