Что такое Deadlock и как его избежать?java-29

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

Условия возникновения Deadlock

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

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

Пример Deadlock

Рассмотрим классический пример Deadlock, где два потока блокируют друг друга:

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

    public void method1() {
        synchronized (lock1) {
            System.out.println("Поток 1 удерживает lock1");
            try {
                Thread.sleep(100); // Имитация работы
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2) {
                System.out.println("Поток 1 удерживает lock2");
            }
        }
    }

    public void method2() {
        synchronized (lock2) {
            System.out.println("Поток 2 удерживает lock2");
            try {
                Thread.sleep(100); // Имитация работы
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock1) {
                System.out.println("Поток 2 удерживает lock1");
            }
        }
    }

    public static void main(String[] args) {
        DeadlockExample example = new DeadlockExample();

        Thread thread1 = new Thread(example::method1);
        Thread thread2 = new Thread(example::method2);

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

Объяснение кода

  1. Поток 1 захватывает lock1 и пытается захватить lock2.
  2. Поток 2 захватывает lock2 и пытается захватить lock1.
  3. Оба потока блокируют друг друга, так как каждый из них удерживает ресурс, который нужен другому потоку. Это приводит к Deadlock.

Как избежать Deadlock?

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

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

Пример:

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

    public void method1() {
        synchronized (lock1) {
            System.out.println("Поток 1 удерживает lock1");
            try {
                Thread.sleep(100); // Имитация работы
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2) {
                System.out.println("Поток 1 удерживает lock2");
            }
        }
    }

    public void method2() {
        synchronized (lock1) { // Теперь оба метода захватывают lock1 первым
            System.out.println("Поток 2 удерживает lock1");
            try {
                Thread.sleep(100); // Имитация работы
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2) {
                System.out.println("Поток 2 удерживает lock2");
            }
        }
    }

    public static void main(String[] args) {
        NoDeadlockExample example = new NoDeadlockExample();

        Thread thread1 = new Thread(example::method1);
        Thread thread2 = new Thread(example::method2);

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

2. Использование тайм-аутов

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

Пример с использованием ReentrantLock:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TimeoutExample {
    private final Lock lock1 = new ReentrantLock();
    private final Lock lock2 = new ReentrantLock();

    public void method1() {
        while (true) {
            if (lock1.tryLock()) {
                try {
                    System.out.println("Поток 1 удерживает lock1");
                    if (lock2.tryLock()) {
                        try {
                            System.out.println("Поток 1 удерживает lock2");
                            break; // Успешно захватили оба ресурса
                        } finally {
                            lock2.unlock();
                        }
                    }
                } finally {
                    lock1.unlock();
                }
            }
            try {
                Thread.sleep(100); // Пауза перед повторной попыткой
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void method2() {
        while (true) {
            if (lock2.tryLock()) {
                try {
                    System.out.println("Поток 2 удерживает lock2");
                    if (lock1.tryLock()) {
                        try {
                            System.out.println("Поток 2 удерживает lock1");
                            break; // Успешно захватили оба ресурса
                        } finally {
                            lock1.unlock();
                        }
                    }
                } finally {
                    lock2.unlock();
                }
            }
            try {
                Thread.sleep(100); // Пауза перед повторной попыткой
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        TimeoutExample example = new TimeoutExample();

        Thread thread1 = new Thread(example::method1);
        Thread thread2 = new Thread(example::method2);

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

3. Использование высокоуровневых инструментов

Для управления сложными сценариями синхронизации можно использовать высокоуровневые инструменты, такие как java.util.concurrent пакет, который предоставляет классы Semaphore, CountDownLatch, CyclicBarrier и другие. Эти инструменты помогают избежать Deadlock, предоставляя более гибкие механизмы синхронизации.

Резюмируем

  • Deadlock — это ситуация, когда потоки блокируют друг друга, ожидая ресурсов, которые удерживаются другими потоками.
  • Чтобы избежать Deadlock, можно использовать следующие подходы:
    1. Упорядоченное получение блокировок: Всегда захватывать ресурсы в определенном порядке.
    2. Использование тайм-аутов: Пытаться захватить блокировку в течение ограниченного времени.
    3. Высокоуровневые инструменты: Использовать классы из пакета java.util.concurrent для управления синхронизацией.

Соблюдение этих принципов поможет вам избежать Deadlock и создавать более надежные многопоточные приложения.