Кейс: два synchronized-метода изредка блокировали друг друга насмерть
Метод перевода денег между счетами блокировал сначала счёт-отправитель, потом счёт-получатель. Под низкой нагрузкой всё работало годами.
Проблема вылезла, когда пользователи стали чаще переводить деньги друг другу во встречных направлениях: A → B и B → A одновременно. Один поток захватывал монитор A и ждал монитор B. Второй в этот момент уже держал монитор B и ждал монитор A. Оба зависли навсегда, threads в статусе BLOCKED, запросы копились за таймаутом клиента.
Thread dump показал ровно это — jstack пишет «Found one Java-level deadlock» с двумя потоками и их мониторами. Без дампа проблему было не поймать: она не воспроизводилась стабильно, только при определённой гонке.
Исправление — не менять сам synchronized, а зафиксировать порядок захвата: всегда сначала счёт с меньшим id.
public void transfer( Account from, Account to, BigDecimal amount) {
Account first = from.getId() < to.getId() ? from : to; Account second = first == from ? to : from;
synchronized (first) { synchronized (second) { from.debit(amount); to.credit(amount); } } }
Дальше на собеседовании спрашивают про альтернативы: tryLock() с таймаутом у java.util.concurrent.locks.Lock, который хотя бы не виснет навечно и может откатиться. Или про то, как вообще обнаружить дедлок в проде без ручного анализа dump — мониторинг threads в состоянии BLOCKED дольше N секунд.
Дедлок почти никогда не в самом synchronized — он в порядке, в котором код захватывает несколько локов подряд.
Тренажёр: 600 вопросов, мок с таймером, план повторов
senior·base — что спрашивают на самом деле
В этом посте были ссылки, но мы их удалили по правилам Сетки