Встречается на 31% собеседований по Java

Расскажи про deadlock

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

Как возникает deadlock

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

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

Если хотя бы одно из этих условий нарушить, deadlock невозможен. На практике чаще всего устраняют циклическое ожидание или используют тайм-ауты.

Пример на Java

Рассмотрим классический пример с двумя потоками и двумя объектами-блокировками:

java
public class DeadlockExample {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();
text
public static void main(String[] args) {
    Thread thread1 = new Thread(() -> {
        synchronized (lockA) {
            System.out.println("Поток 1: захватил lockA");
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            synchronized (lockB) {
                System.out.println("Поток 1: захватил lockB");
            }
        }
    });

    Thread thread2 = new Thread(() -> {
        synchronized (lockB) {
            System.out.println("Поток 2: захватил lockB");
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            synchronized (lockA) {
                System.out.println("Поток 2: захватил lockA");
            }
        }
    });

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

}

text

Здесь поток 1 захватывает `lockA` и ждет `lockB`, а поток 2 захватывает `lockB` и ждет `lockA`. Программа зависнет навсегда.

## Способы предотвращения

### 1. Упорядочивание блокировок

Все потоки должны захватывать ресурсы в одном и том же порядке. Например, всегда сначала `lockA`, потом `lockB`. Тогда циклическое ожидание не возникнет.

### 2. Тайм-ауты при захвате блокировки

Использовать `tryLock` с тайм-аутом вместо безусловного `lock`. Если поток не смог получить блокировку за отведенное время, он освобождает все свои текущие блокировки и повторяет попытку.

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

if (lockA.tryLock(100, TimeUnit.MILLISECONDS)) {
    try {
        if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                // работа с ресурсами
            } finally {
                lockB.unlock();
            }
        }
    } finally {
        lockA.unlock();
    }
}

3. Обнаружение deadlock

Система может периодически проверять наличие циклических ожиданий и принудительно прерывать один из потоков (например, с помощью Thread.interrupt() или освобождения ресурса). В Java есть встроенные средства мониторинга, такие как ThreadMXBean.findDeadlockedThreads().

4. Использование однопоточных моделей

В некоторых случаях можно избежать многопоточности, используя асинхронное программирование (например, CompletableFuture) или реактивные подходы, где нет блокирующих ожиданий.

Подводные камни

  • Deadlock трудно воспроизвести, так как он зависит от тайминга потоков. Даже если программа работает стабильно, это не гарантирует отсутствие потенциального deadlock.
  • Использование synchronized не позволяет задать тайм-аут, поэтому для гибкого управления лучше применять ReentrantLock.
  • При использовании нескольких блокировок всегда следите за порядком их захвата и освобождения.

Коротко

  • Deadlock — это взаимная блокировка потоков, ожидающих ресурсы друг друга.
  • Для возникновения нужны четыре условия: взаимное исключение, удержание и ожидание, невытесняемость, циклическое ожидание.
  • Основные методы борьбы: упорядочивание блокировок, тайм-ауты, обнаружение deadlock, однопоточные модели.
  • На собеседовании приведите пример на Java и объясните, как его избежать.
как отвечать на вопрос
пример собеседования
фреймворки на собеседовании
типичные вопросы junior
интервью вопросы и ответы