Перейти к основному содержимому

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

· 5 мин. чтения

1. Обзор

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

2. Тупик

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

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

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

2.2. Пример взаимоблокировки

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

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

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

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

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 ожидает lock1 , удерживаемый потоком T1 .

2.3. Как избежать взаимоблокировки

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

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

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

3. Лайвлок

3.1. Что такое Лайвлок

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

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

3.2. Пример блокировки

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

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

Давайте продемонстрируем livelock с классом LivelockExample :

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 .