Що таке 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]]