Кейс: два 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 — что спрашивают на самом деле


В этом посте были ссылки, но мы их удалили по правилам Сетки