Что такое deadlock (взаимная блокировка) в Java и как её предотвратить?

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

Ответ

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

Условия возникновения deadlock'а (Коффмана):

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

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

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

    public static void main(String[] args) {
        Thread threadA = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread A: Holding lock 1...");
                try { Thread.sleep(10); } catch (InterruptedException e) {}
                System.out.println("Thread A: Waiting for lock 2...");
                synchronized (lock2) { // Блокируется здесь
                    System.out.println("Thread A: Acquired lock 2!");
                }
            }
        });

        Thread threadB = new Thread(() -> {
            synchronized (lock2) { // Порядок захвата обратный!
                System.out.println("Thread B: Holding lock 2...");
                try { Thread.sleep(10); } catch (InterruptedException e) {}
                System.out.println("Thread B: Waiting for lock 1...");
                synchronized (lock1) { // Блокируется здесь
                    System.out.println("Thread B: Acquired lock 1!");
                }
            }
        });

        threadA.start();
        threadB.start(); // Программа зависнет в deadlock'е.
    }
}

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

  1. Упорядочивание блокировок: Всегда захватывать несколько блокировок в строго определённом глобальном порядке (например, сначала lock1, потом lock2). Это нарушает условие циклического ожидания.
  2. Использование tryLock с таймаутом: В java.util.concurrent.locks.ReentrantLock используйте метод tryLock(long timeout, TimeUnit unit), который позволяет потоку отказаться от ожидания блокировки.
    if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
        try {
            if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                try {
                    // Критическая секция
                } finally { lock2.unlock(); }
            }
        } finally { lock1.unlock(); }
    }
  3. Избегание вложенных блокировок: Проектировать код так, чтобы потоку требовалась только одна блокировка за раз.
  4. Использование высокоуровневых примитивов: Замена ручной синхронизации на потокобезопасные коллекции из java.util.concurrent (например, ConcurrentHashMap), ExecutorService, CountDownLatch и т.д.