Что такое deadlock (взаимная блокировка)?
Два человека на узком мосту с разных сторон:
Junior уровень
Базовое понимание
Deadlock (взаимная блокировка) — это ситуация, когда два или более потока блокируют друг друга навсегда.
В коде: Поток 1 держит lockA и ждёт lockB. Поток 2 держит lockB и ждёт lockA. Ни один не отпустит свой lock первым. Оба потока зависли навсегда.
Простая аналогия
Два человека на узком мосту с разных сторон:
- Человек A: “Я не пропущу тебя, пока ты не пропустишь меня”
- Человек B: “Я не пропущу тебя, пока ты не пропустишь меня”
- Результат: Оба стоят вечно
Пример в Java
public class DeadlockDemo {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void methodA() {
synchronized(lock1) {
System.out.println("Поток 1: Захватил lock1");
// Пауза, чтобы другой поток успел захватить lock2
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized(lock2) { // ЖДЁМ lock2 — но он у другого потока!
System.out.println("Поток 1: Захватил lock2");
}
}
}
public void methodB() {
synchronized(lock2) {
System.out.println("Поток 2: Захватил lock2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized(lock1) { // ЖДЁМ lock1 — но он у другого потока!
System.out.println("Поток 2: Захватил lock1");
}
}
}
}
// Запуск:
new Thread(demo::methodA).start();
new Thread(demo::methodB).start();
// → DEADLOCK! Поток 1 ждёт lock2, Поток 2 ждёт lock1
Deadlock vs Livelock vs Starvation
| Тип | Поведение | CPU | |
|---|---|---|---|
| Deadlock | Потоки “спят” и никогда не проснутся | 0% | |
| Livelock | Потоки постоянно реагируют друг на друга, но не делают прогресса | 100% | |
| Starvation | Поток готов к работе, но никогда не получает ресурс | Зависит |
Livelock пример
// Два человека в коридоре — оба пытаются уступить, но в одну сторону
while (otherPersonMoving) {
stepAside(); // Постоянно реагируем, но не проходим
}
Starvation пример
// Поток с низким приоритетом никогда не получает CPU
Thread lowPriority = new Thread(task);
lowPriority.setPriority(Thread.MIN_PRIORITY); // Всегда ждёт
Middle уровень
Wait-for-Graph (Граф ожидания)
В теории deadlock представляется как цикл в графе:
Поток 1 ──ждёт──→ Lock B ──держит──→ Поток 2
▲ │
│ │
└───────────ждёт──────── Lock A ◄───────┘
держит
ЦИКЛ = DEADLOCK
Условия Коффмана (Coffman Conditions)
Deadlock возможен ТОЛЬКО при одновременном выполнении всех 4 условий:
| # | Условие | Описание |
|---|---|---|
| 1 | Mutual Exclusion | Ресурс может быть занят только одним потоком |
| 2 | Hold and Wait | Поток держит один ресурс и ждёт другой |
| 3 | No Preemption (отсутствие принудительного изъятия) — если поток захватил synchronized(lock), никто не может насильно забрать у него этот lock. Только сам поток может выйти из synchronized. |
|
| 4 | Circular Wait | Существует замкнутая цепочка ожиданий |
Database Deadlocks
Deadlock может возникнуть и на уровне БД:
// Поток 1:
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
// Поток 2:
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
// → DEADLOCK! БД обнаружит и откатит одну транзакцию
Отличие: БД умеет детектировать deadlock и автоматически откатывает одну транзакцию. В Java вам придётся детектировать вручную.
Hidden Deadlock (Скрытый)
// Deadlock через Thread Pool Starvation
ExecutorService pool = Executors.newFixedThreadPool(1);
// Пошаговый разбор:
// Шаг 1: pool.submit(A) — единственный поток начал выполнять задачу А
// Шаг 2: Внутри А вызывается pool.submit(B).get() — задача Б поставлена в очередь
// Шаг 3: .get() блокирует поток А в ожидании Б
// Шаг 4: Поток единственный и занят, Б никогда не начнётся = DEADLOCK
// Это называется "thread pool starvation deadlock"
Future<String> future = pool.submit(() -> {
// Эта задача ждёт другую задачу из того же пула
return pool.submit(() -> "result").get(); // DEADLOCK!
});
// Один поток занят первой задачей, вторая задача ждёт в очереди
Static Initialization Deadlock
// Класс A
class A {
static {
System.out.println(B.value); // Обращается к B
}
}
// Класс B
class B {
static {
System.out.println(A.value); // Обращается к A
}
}
// Два потока загружают A и B одновременно → deadlock в ClassLoader
Senior уровень
Когда НЕ использовать вложенные synchronized
- Порядок захвата нельзя гарантировать — разные пути кода захватывают locks в разном порядке
- Есть внешние вызовы внутри блокировки (БД, HTTP, файловая система) — внешний ресурс может заблокировать и создать deadlock
- Неизвестный код — передаёте lock в чужой метод, который может захватить другой lock
Альтернативы: ReentrantLock.tryLock(timeout), lock-free структуры (ConcurrentHashMap, AtomicReference), или один общий lock.
Under the Hood: JVM Deadlock Detection
JVM имеет встроенный детектор deadlock через ThreadMXBean:
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = mxBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
for (long threadId : deadlockedThreads) {
ThreadInfo info = mxBean.getThreadInfo(threadId, true, true);
System.out.println("Deadlock: " + info.getThreadName());
System.out.println("Blocked on: " + info.getLockName());
System.out.println("Blocked by: " + info.getLockOwnerName());
}
}
Важно: Это дорогая операция — не запускайте слишком часто.
Deadlock в Highload
Проблема: Deadlock может парализовать систему за миллисекунды
Детектор JVM: Работает периодически (не мгновенно)
Результат: потоки, которым нужны заблокированные ресурсы, зависнут навсегда. Остальная система продолжит работать, но пропускная способность упадёт.
Edge Cases
Deadlock с внешними системами
Java поток: держит lockA → ждёт ответ от БД
БД транзакция: держит lock на таблицу → ждёт HTTP от Java приложения
→ DEADLOCK между Java и БД!
ReentrantLock и interruption
// synchronized нельзя прервать — поток будет висеть до перезагрузки
synchronized(lock) {
// Deadlock здесь = вечное ожидание
}
// ReentrantLock можно прервать
lock.lockInterruptibly();
try {
// Deadlock здесь = можно прервать через thread.interrupt()
} finally {
lock.unlock();
}
Диагностика
jstack — самый быстрый способ
jstack -l <pid>
Вывод:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f... (object 0x000000076af0c8d0),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x00007f... (object 0x000000076af0c8e0),
which is held by "Thread-1"
VisualVM / JConsole
Кнопка “Check Deadlock” — мгновенная проверка.
Программная проверка
// Периодическая проверка (каждые 30 секунд)
ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor();
monitor.scheduleAtFixedRate(() -> {
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
long[] deadlocked = mxBean.findDeadlockedThreads();
if (deadlocked != null) {
log.error("DEADLOCK DETECTED! Threads: {}", Arrays.toString(deadlocked));
// Alert, restart, etc.
}
}, 0, 30, TimeUnit.SECONDS);
Prevention Rule: Lock Ordering
Самый эффективный способ предотвращения — всегда захватывайте ресурсы в строго определённом порядке:
// Определяем порядок через hashCode
void transfer(Account from, Account to, int amount) {
Object first = System.identityHashCode(from) < System.identityHashCode(to) ? from : to;
Object second = first == from ? to : from;
synchronized(first) {
synchronized(second) {
from.withdraw(amount);
to.deposit(amount);
}
}
}
Best Practices
- Lock Ordering — всегда захватывайте ресурсы в определённом порядке
- tryLock(timeout) — используйте ReentrantLock с таймаутом
- Избегайте вложенных блокировок — если возможно
- Lock-free структуры — ConcurrentHashMap, Atomic*
- Thread-specific resources — шардирование, независимые данные
- Мониторьте через ThreadMXBean.findDeadlockedThreads()
- jstack — первый инструмент при подозрении на deadlock
- Используйте ReentrantLock — вместо synchronized для прерываемости
🎯 Шпаргалка для интервью
Обязательно знать:
- Deadlock = два потока блокируют друг друга навсегда (Поток 1 держит lockA и ждёт lockB, Поток 2 держит lockB и ждёт lockA)
- 4 условия Коффмана (все одновременно): Mutual Exclusion, Hold and Wait, No Preemption, Circular Wait
- Deadlock vs Livelock vs Starvation: deadlock — потоки спят (0% CPU), livelock — реагируют но без прогресса (100% CPU), starvation — поток никогда не получает ресурс
- Lock Ordering — главный способ предотвращения: всегда захватывайте ресурсы в строго определённом порядке (например, по hashCode)
- ReentrantLock.tryLock(timeout) — позволяет избежать вечного ожидания, в отличие от synchronized
- JVM детектирует deadlock через ThreadMXBean.findDeadlockedThreads(), но это дорогая операция
- Скрытый deadlock: thread pool starvation — задача ждёт результат другой задачи из того же пула (особенно single-thread pool)
- БД автоматически детектирует и откатывает deadlock, Java — нет (нужен ручной мониторинг)
Частые уточняющие вопросы:
- Как предотвратить deadlock? — Lock ordering (фиксированный порядок захвата), tryLock(timeout), lock-free структуры, один общий lock
- Чем synchronized отличается от ReentrantLock при deadlock? — synchronized нельзя прервать (вечное ожидание), ReentrantLock.lockInterruptibly() можно прервать через thread.interrupt()
- Что такое thread pool starvation deadlock? — Задача в пуле ждёт .get() от другой задачи в том же пуле; если пул single-thread — deadlock гарантирован
- Как обнаружить deadlock в production? — jstack -l
, ThreadMXBean.findDeadlockedThreads(), VisualVM «Check Deadlock»
Красные флаги (НЕ говорить):
- “Deadlock невозможен в моём коде” — он часто скрытый (вложенные synchronized, внешние вызовы)
- “JVM автоматически разрешает deadlock” — нет, JVM только детектирует, но не разрешает (в отличие от БД)
- “shutdownNow() решит deadlock” — нет, он посылает interrupt(), но synchronized нельзя прервать
- “Достаточно одного lock для предотвращения” — нет, deadlock возникает при 2+ ресурсах в разном порядке
Связанные темы:
- [[10. Как работают AtomicInteger, AtomicLong]]
- [[11. В чём преимущество Atomic классов перед synchronized]]
- [[12. Что такое пул потоков (Thread Pool)]]
- [[15. Что делает ExecutorService]]