Вот код с двумя блокировками, который иногда зависает. Найдите deadlock и предложите исправления.
Короткий ответ
- Deadlock: потоки захватывают те же локи в разном порядке
- Четыре условия Коффмана, на практике ломают циклическое ожидание
- Главный фикс — глобальный порядок захвата блокировок
- Альтернатива: tryLock с таймаутом и отступлением
- Лучше всего — перепроектировать, чтобы не держать два лока
- Диагностика: jstack / jcmd Thread.print находит deadlock сам
Классический deadlock от разнопорядкового захвата локов лечится единым порядком захвата или tryLock с откатом.
Как сказать вслух
пример ответаЗдесь поток А захватывает первый замок и ждёт второй, а поток Б — наоборот: захватил второй и ждёт первый. Оба ждут вечно — это взаимная блокировка. Самое надёжное исправление — договориться о едином порядке: все потоки берут замки в одной и той же последовательности, например по идентификатору счёта. Другой вариант — tryLock с таймаутом: не получил второй замок — отпусти первый и повтори. А диагностируется это просто: jstack прямо пишет «Found one Java-level deadlock».
Подробный ответ
Основной ответ
В типовом примере transfer(from, to) синхронизируется сначала на from, потом на to; два встречных перевода A→B и B→A захватывают мониторы в противоположном порядке и взаимно блокируются. Условия deadlock (взаимное исключение, удержание с ожиданием, отсутствие вытеснения, циклическое ожидание) — ломать проще всего последнее. Исправления: 1) глобальный порядок захвата — упорядочить ресурсы по стабильному ключу (id счёта, System.identityHashCode с tie-breaker-локом) и всегда брать «меньший» первым; 2) ReentrantLock.tryLock(timeout) — при неудаче освободить всё и повторить (возможен livelock, добавляют случайную задержку); 3) убрать вложенные блокировки вовсе: один общий лок на операцию, неизменяемые структуры, очередь команд к единственному владельцу состояния. Диагностика: jstack/jcmd Thread.print печатает цикл ожидания, ThreadMXBean.findDeadlockedThreads — программно.
Ключевые моменты
- Корень бага. Разный порядок захвата одних и тех же локов в разных потоках — цикл ожидания.
- Lock ordering. Единый порядок по стабильному ключу гарантированно исключает цикл; это фикс по умолчанию.
- tryLock. Неблокирующий захват с таймаутом и откатом; следить за livelock и честностью.
- Диагностика. jstack пишет deadlock явно; в проде помогают мониторинг зависших потоков и JFR.
Практический контекст
Классика senior-секции по многопоточности, обычно на примере перевода денег между счетами. Интервьюер смотрит, назовёт ли кандидат порядок захвата как основной фикс, вспомнит ли jstack и сможет ли рассуждать об архитектурных альтернативах — не держать два лока, свести изменения к одному владельцу, использовать БД-транзакции вместо локов в памяти. Упоминание livelock при наивном tryLock — заметный плюс.
Пример кода
// БАГ: встречные переводы берут локи в разном порядке
void transfer(Account from, Account to, long amount) {
synchronized (from) {
synchronized (to) { // A->B и B->A зависнут
from.withdraw(amount);
to.deposit(amount);
}
}
}
// FIX: единый порядок захвата по id
void transferSafe(Account from, Account to, long amount) {
Account first = from.id() < to.id() ? from : to;
Account second = first == from ? to : from;
synchronized (first) {
synchronized (second) {
from.withdraw(amount);
to.deposit(amount);
}
}
}Частые ошибки
- Предлагают «просто synchronized на метод», создавая глобальную точку конкуренции, либо не замечая, что локи разные
- Фиксят tryLock-ом без случайной задержки и получают livelock
- Не могут объяснить, как подтвердить deadlock в работающем приложении