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

Ответ

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

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

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

Пример на Java:

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

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1: Holding lock1...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread 1: Waiting for lock2...");
                synchronized (lock2) { // Блокировка: lock2 удерживается t2
                    System.out.println("Thread 1: Acquired lock2!");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2: Holding lock2...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread 2: Waiting for lock1...");
                synchronized (lock1) { // Блокировка: lock1 удерживается t1
                    System.out.println("Thread 2: Acquired lock1!");
                }
            }
        });

        t1.start();
        t2.start(); // Потоки зависнут в deadlock.
    }
}

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

  • Упорядоченный захват ресурсов: Всегда захватывайте блокировки в строго одинаковом глобальном порядке (например, сначала lock1, потом lock2).
  • Использование tryLock с таймаутом: Вместо synchronized используйте ReentrantLock.tryLock(long timeout, TimeUnit unit) из пакета java.util.concurrent.locks.
  • Сокращение области видимости блокировки: Удерживайте блокировки минимально необходимое время.
  • Обнаружение и восстановление: Используйте мониторинг для выявления deadlock'ов (например, через ThreadMXBean.findDeadlockedThreads()) и принудительного прерывания одного из потоков.

Ответ 18+ 🔞

О, смотри-ка, какая интересная хуйня — взаимная блокировка! Это, блядь, как два упыря в подъезде стоят: один говорит «ты первый проходи», а второй — «нет, ты первый, я постою». И стоят они так до скончания времён, потому что оба ждут, пока другой сдвинется с места. В программировании то же самое, только вместо упырей — потоки, а вместо подъезда — ресурсы какие-нибудь, типа файлов или объектов.

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

  1. Взаимное исключение — ресурс, как последняя пачка сигарет, на всех не хватит. Один взял — остальные ждут.
  2. Удержание и ожидание — поток уже что-то схватил в свои цепкие лапки, но жадничает и тянется за вторым, не отпуская первое.
  3. Невозможность отъёма — отобрать ресурс насильно нельзя, только если сам поток его отпустит. Как у ребёнка конфету — пока не отдаст, не получит.
  4. Циклическое ожидание — вот тут пиздец начинается. Поток А ждёт ресурс у потока Б, а поток Б ждёт ресурс у потока А. Замкнутый круг, хуле. Все стоят и тупо смотрят друг на друга.

Смотри, как это выглядит в коде, прям классика жанра:

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

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1: Holding lock1...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread 1: Waiting for lock2...");
                synchronized (lock2) { // А lock2-то уже в лапах у t2! Засада!
                    System.out.println("Thread 1: Acquired lock2!");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2: Holding lock2...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread 2: Waiting for lock1...");
                synchronized (lock1) { // А lock1, блядь, уже приватизировал t1! Пиздец!
                    System.out.println("Thread 2: Acquired lock1!");
                }
            }
        });

        t1.start();
        t2.start(); // Запускаем эту вечеринку и... всё, приехали. Программа просто повиснет.
    }
}

Запустишь это дело — и она просто встанет колом, будет висеть вечно, как твои надежды на светлое будущее. Потоки t1 и t2 друг друга заблокируют нахуй.

Как не попасть в такую позорную ситуацию? Ну, варианты есть, конечно:

  • Порядок, сука, порядок! Всегда хватай блокировки в одном и том же порядке. Скажем, сначала всегда lock1, а потом уже lock2. Тогда цикла не получится — один поток схватит первую, а второй будет ждать, пока она освободится, а не лезть за второй.
  • Не жадничай, попробуй! Используй ReentrantLock с методом tryLock(). Он как вежливый человек: «Можно я ресурс возьму?» — и если не дают, он не стоит как идиот, а идёт делать другие дела или ждёт, но не больше заданного времени. Красота!
  • Быстро схватил — быстро отпустил. Держи блокировку ровно столько, сколько нужно, не растягивай удовольствие. Чем дольше держишь, тем выше шанс, что кто-то ещё захочет этот ресурс и начнётся драка.
  • Выследить и пристрелить. Можно поставить надзирателя (ThreadMXBean), который будет смотреть, не образовался ли deadlock. Обнаружил — принимай меры: один из потоков в жопу ногой (interrupt()), чтобы разорвать этот порочный круг.

Вот и вся наука. Главное — не создавай условий для этой ебли, а то потом будешь как Герасим с Муму — стоять на берегу и понимать, что всё проебал, но уже поздно.