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

Ответ

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

Необходимые условия (условия Коффмана):

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

Классический пример на Java:

Object lockA = new Object();
Object lockB = new Object();

Thread thread1 = new Thread(() -> {
    synchronized (lockA) {          // 1. thread1 захватывает lockA
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        synchronized (lockB) {      // 3. thread1 ждёт lockB (его держит thread2)
            System.out.println("Thread 1");
        }
    }
});

Thread thread2 = new Thread(() -> {
    synchronized (lockB) {          // 2. thread2 захватывает lockB
        synchronized (lockA) {      // 4. thread2 ждёт lockA (его держит thread1) -> DEADLOCK
            System.out.println("Thread 2");
        }
    }
});

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

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

  • Упорядочивание блокировок: Всегда захватывать блокировки в одном и том же глобальном порядке (например, сначала lockA, потом lockB).
  • Try-Lock с таймаутом: Использовать tryLock() из java.util.concurrent.locks.ReentrantLock с ожиданием.
  • Отказ от вложенных блокировок: Перепроектировать логику, чтобы не требовалось удерживать несколько блокировок.
  • Обнаружение и восстановление: Периодически проверять систему на deadlock и принудительно завершать один из потоков (редко используется в прикладном коде).

Ответ 18+ 🔞

А, ну это же классика, ёпта! Взаимная блокировка, она же deadlock — это когда два потока или процесса так друг другу в сраку въехали, что оба встали колом и ждут до второго пришествия. Представь: каждый держит в одной руке то, что нужно другому, а второй рукой пытается это у соседа вырвать. И оба упёрлись, как бараны, и стоят. Пиздец, а не работа.

Чтобы этот цирк случился, нужно аж четыре условия, как в плохом анекдоте:

  1. Взаимное исключение. Ресурс — штука единоличная, как последняя пачка гречки в магазине. Или я, или никто.
  2. Удержание и ожидание. Я уже эту гречку в руках держу, но при этом ещё и на соседнюю полку с тушёнкой глаз положил, жду, когда её отпустят.
  3. Отсутствие вытеснения. У меня гречку просто так не отнять. Никакого «отдай, потом разберёмся». Хитрая жопа, да?
  4. Циклическое ожидание. И вот тут начинается магия: я жду твою тушёнку, ты ждёшь мою гречку, а третий ждёт, пока мы оба передохнем. Круг замкнулся, ебать!

Вот смотри, как это на Java выглядит, прям учебник позора:

Object lockA = new Object();
Object lockB = new Object();

Thread thread1 = new Thread(() -> {
    synchronized (lockA) {          // 1. thread1 хватает lockA
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        synchronized (lockB) {      // 3. thread1 тупо ждёт lockB (а его уже thread2 прихватизировал)
            System.out.println("Thread 1");
        }
    }
});

Thread thread2 = new Thread(() -> {
    synchronized (lockB) {          // 2. thread2, не долго думая, хватает lockB
        synchronized (lockA) {      // 4. thread2 орёт "дай lockA!" (а его держит thread1). ВСЁ. ТУПИК.
            System.out.println("Thread 2");
        }
    }
});

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

Запустишь этот код — и они оба повиснут, как два идиота, уставившись друг на друга. Никто не напишет свою строчку. Вечная тишина, блядь.

Как не наступить на эти грабли, чтобы потом не выковыривать их из жопы?

  • Упорядочивание блокировок, ёпта! Самый надёжный способ. Договорись сам с собой, что всегда будешь хватать замки в одном порядке. Сначала lockA, потом lockB. И никаких «ой, а тут удобнее с lockB начать». Нельзя! Тогда циклического ожидания не будет. Один поток схватит A, второй попробует — ага, A занят, значит, стоп, ждём своей очереди. Порядок, сука!
  • Try-Lock с таймаутом. Не хочешь ждать вечно? Используй ReentrantLock с методом tryLock(). Попробовал взять замок — не получилось за определённое время? Отвалил, освободил всё, что держал, подумал, перезарядился и попробовал снова. Не упорствуй, как Герасим с Муму.
  • Не плоди сущностей. А может, вообще перепроектировать логику, чтобы не нужно было держать кучу блокировок одновременно? Иногда это лучший выход. Волнение ебать, но мозги надо включать.
  • Обнаружение и восстановление. Это уже для отчаянных парней. Пусть система периодически смотрит — а не образовалась ли у нас тут петля из идиотов? Обнаружила — одного пристрелила (завершила), остальные, обрадованные, побежали дальше. В обычном коде такое — редкость, овердохуища мороки.

Короче, deadlock — это не баг, это вызов твоей способности думать головой, а не жопой. Просто помни про эти четыре условия и не создавай их все сразу. Хуй с горы, если нарушишь одно — уже безопаснее.