Рубрики
Без рубрики

Взаимоблокировка потоков Java и Livelock

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

Автор оригинала: Kamlesh Kumar.

1. Обзор

Хотя многопоточность помогает повысить производительность приложения, она также сопряжена с некоторыми проблемами. В этом уроке мы рассмотрим две такие проблемы, deadlock и livelock, с помощью примеров Java.

2. Тупик

2.1. Что Такое Тупик?

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

Классическая проблема обедающих философов прекрасно демонстрирует проблемы синхронизации в многопоточной среде и часто используется в качестве примера тупика.

2.2. Пример Тупиковой Ситуации

Во-первых, давайте рассмотрим простой пример Java, чтобы понять тупик.

В этом примере мы создадим два потока: T1 и T2 . Поток T1 вызывает операцию 1 , а поток T2 вызывает операции .

Чтобы завершить свои операции, поток T1 должен сначала получить lock 1 , а затем lock 2 , тогда как поток T2 должен сначала получить lock2 , а затем lock1 . Таким образом, в основном, оба потока пытаются получить блокировки в противоположном порядке.

Теперь давайте напишем класс Deadlock Example :

public class DeadlockExample {

    private Lock lock1 = new ReentrantLock(true);
    private Lock lock2 = new ReentrantLock(true);

    public static void main(String[] args) {
        DeadlockExample deadlock = new DeadlockExample();
        new Thread(deadlock::operation1, "T1").start();
        new Thread(deadlock::operation2, "T2").start();
    }

    public void operation1() {
        lock1.lock();
        print("lock1 acquired, waiting to acquire lock2.");
        sleep(50);

        lock2.lock();
        print("lock2 acquired");

        print("executing first operation.");

        lock2.unlock();
        lock1.unlock();
    }

    public void operation2() {
        lock2.lock();
        print("lock2 acquired, waiting to acquire lock1.");
        sleep(50);

        lock1.lock();
        print("lock1 acquired");

        print("executing second operation.");

        lock1.unlock();
        lock2.unlock();
    }

    // helper methods

}

Давайте теперь запустим этот пример взаимоблокировки и заметим результат:

Thread T1: lock1 acquired, waiting to acquire lock2.
Thread T2: lock2 acquired, waiting to acquire lock1.

Как только мы запустим программу, мы увидим, что она приводит к тупику и никогда не выходит. Журнал показывает , что поток T1 ждет блокировки 2 , которая удерживается потоком T2 . Аналогично, поток T2 ожидает блокировки 1 , которая удерживается потоком T1 .

2.3. Как избежать Тупика

Взаимоблокировка – это распространенная проблема параллелизма в Java. Поэтому мы должны разработать Java-приложение, чтобы избежать любых потенциальных тупиковых условий.

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

Мы также можем использовать timed lock attempts , как метод tryLock в интерфейсе Lock , чтобы убедиться, что поток не блокируется бесконечно, если он не может получить блокировку.

3. Живой замок

3.1. Что Такое Lifelock

Livelock-это еще одна проблема параллелизма, похожая на тупик. В livelock два или более потока продолжают передавать состояния друг другу вместо бесконечного ожидания, как мы видели в примере с тупиком. Следовательно, потоки не могут выполнять свои соответствующие задачи.

Отличным примером livelock является система обмена сообщениями, в которой при возникновении исключения потребитель сообщения откатывает транзакцию и помещает сообщение обратно в начало очереди. Затем одно и то же сообщение повторно считывается из очереди, только чтобы вызвать другое исключение и быть помещенным обратно в очередь. Потребитель никогда не получит никакого другого сообщения из очереди.

3.2. Пример Livelock

Теперь, чтобы продемонстрировать состояние живого потока, мы возьмем тот же самый пример тупика, который мы обсуждали ранее. В этом примере также поток T1 вызывает операцию 1 и поток T2 вызывает операцию 2 . Однако логику этих операций мы немного изменим.

Обе нити нуждаются в двух блокировках, чтобы завершить свою работу. Каждый поток получает свою первую блокировку, но обнаруживает, что вторая блокировка недоступна. Таким образом, чтобы позволить другому потоку завершить работу первым, каждый поток освобождает свою первую блокировку и пытается снова получить обе блокировки.

Давайте продемонстрируем livelock с помощью примера Livelock класса:

public class LivelockExample {

    private Lock lock1 = new ReentrantLock(true);
    private Lock lock2 = new ReentrantLock(true);

    public static void main(String[] args) {
        LivelockExample livelock = new LivelockExample();
        new Thread(livelock::operation1, "T1").start();
        new Thread(livelock::operation2, "T2").start();
    }

    public void operation1() {
        while (true) {
            tryLock(lock1, 50);
            print("lock1 acquired, trying to acquire lock2.");
            sleep(50);

            if (tryLock(lock2)) {
                print("lock2 acquired.");
            } else {
                print("cannot acquire lock2, releasing lock1.");
                lock1.unlock();
                continue;
            }

            print("executing first operation.");
            break;
        }
        lock2.unlock();
        lock1.unlock();
    }

    public void operation2() {
        while (true) {
            tryLock(lock2, 50);
            print("lock2 acquired, trying to acquire lock1.");
            sleep(50);

            if (tryLock(lock1)) {
                print("lock1 acquired.");
            } else {
                print("cannot acquire lock1, releasing lock2.");
                lock2.unlock();
                continue;
            }

            print("executing second operation.");
            break;
        }
        lock1.unlock();
        lock2.unlock();
    }

    // helper methods

}

Теперь давайте рассмотрим этот пример:

Thread T1: lock1 acquired, trying to acquire lock2.
Thread T2: lock2 acquired, trying to acquire lock1.
Thread T1: cannot acquire lock2, releasing lock1.
Thread T2: cannot acquire lock1, releasing lock2.
Thread T2: lock2 acquired, trying to acquire lock1.
Thread T1: lock1 acquired, trying to acquire lock2.
Thread T1: cannot acquire lock2, releasing lock1.
Thread T1: lock1 acquired, trying to acquire lock2.
Thread T2: cannot acquire lock1, releasing lock2.
..

Как мы видим в журналах, оба потока неоднократно получают и освобождают блокировки. Из-за этого ни один из потоков не может завершить операцию.

3.3. Избегание живого потока

Чтобы избежать оживления, нам нужно изучить состояние, которое вызывает оживление, а затем придумать соответствующее решение.

Например, если у нас есть два потока, которые постоянно получают и освобождают блокировки, что приводит к оживлению, мы можем спроектировать код так, чтобы потоки повторяли попытки получения блокировок через случайные интервалы. Это даст потокам справедливый шанс приобрести нужные им замки.

Другой способ решить проблему живучести в примере с системой обмена сообщениями, который мы обсуждали ранее, – поместить неудачные сообщения в отдельную очередь для дальнейшей обработки вместо того, чтобы снова помещать их в ту же очередь.

4. Заключение

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

Как всегда, полный код, используемый в этом примере, можно найти на GitHub .