Расскажи про deadlock
Deadlock (взаимная блокировка) — это ситуация в многопоточном программировании, при которой два или более потоков вечно ожидают друг друга, освобождая занятые ресурсы. Каждый поток удерживает блокировку на одном ресурсе и одновременно пытается получить блокировку на другом ресурсе, который уже захвачен другим потоком. В результате ни один поток не может продолжить выполнение, и программа зависает.
Как возникает deadlock
Для возникновения deadlock необходимо выполнение четырех условий (условия Коффмана):
- Взаимное исключение: ресурс может быть занят только одним потоком в данный момент.
- Удержание и ожидание: поток, удерживающий ресурс, может запрашивать другие ресурсы.
- Невытесняемость: ресурс не может быть принудительно отобран у потока; он должен быть освобожден самим потоком.
- Циклическое ожидание: существует цикл потоков, где каждый поток ожидает ресурс, удерживаемый следующим потоком в цикле.
Если хотя бы одно из этих условий нарушить, deadlock невозможен. На практике чаще всего устраняют циклическое ожидание или используют тайм-ауты.
Пример на Java
Рассмотрим классический пример с двумя потоками и двумя объектами-блокировками:
public class DeadlockExample {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Поток 1: захватил lockA");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) {
System.out.println("Поток 1: захватил lockB");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Поток 2: захватил lockB");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockA) {
System.out.println("Поток 2: захватил lockA");
}
}
});
thread1.start();
thread2.start();
}}
Здесь поток 1 захватывает `lockA` и ждет `lockB`, а поток 2 захватывает `lockB` и ждет `lockA`. Программа зависнет навсегда.
## Способы предотвращения
### 1. Упорядочивание блокировок
Все потоки должны захватывать ресурсы в одном и том же порядке. Например, всегда сначала `lockA`, потом `lockB`. Тогда циклическое ожидание не возникнет.
### 2. Тайм-ауты при захвате блокировки
Использовать `tryLock` с тайм-аутом вместо безусловного `lock`. Если поток не смог получить блокировку за отведенное время, он освобождает все свои текущие блокировки и повторяет попытку.
```java
ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();
if (lockA.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// работа с ресурсами
} finally {
lockB.unlock();
}
}
} finally {
lockA.unlock();
}
}3. Обнаружение deadlock
Система может периодически проверять наличие циклических ожиданий и принудительно прерывать один из потоков (например, с помощью Thread.interrupt() или освобождения ресурса). В Java есть встроенные средства мониторинга, такие как ThreadMXBean.findDeadlockedThreads().
4. Использование однопоточных моделей
В некоторых случаях можно избежать многопоточности, используя асинхронное программирование (например, CompletableFuture) или реактивные подходы, где нет блокирующих ожиданий.
Подводные камни
- Deadlock трудно воспроизвести, так как он зависит от тайминга потоков. Даже если программа работает стабильно, это не гарантирует отсутствие потенциального deadlock.
- Использование
synchronizedне позволяет задать тайм-аут, поэтому для гибкого управления лучше применятьReentrantLock. - При использовании нескольких блокировок всегда следите за порядком их захвата и освобождения.
Коротко
- Deadlock — это взаимная блокировка потоков, ожидающих ресурсы друг друга.
- Для возникновения нужны четыре условия: взаимное исключение, удержание и ожидание, невытесняемость, циклическое ожидание.
- Основные методы борьбы: упорядочивание блокировок, тайм-ауты, обнаружение deadlock, однопоточные модели.
- На собеседовании приведите пример на Java и объясните, как его избежать.