← Назад к списку
ПрограммированиеJava и KotlinSenior

Вот код с двумя блокировками, который иногда зависает. Найдите 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 в работающем приложении

ИП Кочкин Алексей Сергеевич · ИНН 390509026279 · ОГРНИП 325390000030973 · jiniys2005@yandex.ru