Определите, почему два рабочих потока перестали продвигаться, по этому thread dump
Платёжный сервис перестал проводить переводы. Остальные потоки пула ещё обслуживают запросы, загрузка CPU почти нулевая, ни одного исключения в логах, а два рабочих потока ниже больше не сдвигаются. Вы снимаете thread dump.
Ограничения:
- рассуждайте только по дампу — подключить отладчик к продакшену нельзя
- объясните, чего ждут эти два потока, и назовите исправление
"worker-1" #21 prio=5 tid=0x00007f9c0a12c800 nid=0x2b03 waiting for monitor entry
java.lang.Thread.State: BLOCKED (on object monitor)
at com.acme.billing.Ledger.credit(Ledger.java:88)
- waiting to lock <0x000000076ab3f2c8> (a com.acme.billing.Account)
- locked <0x000000076ab41180> (a com.acme.billing.Account)
at com.acme.billing.Ledger.transfer(Ledger.java:61)
"worker-2" #22 prio=5 tid=0x00007f9c0a12f000 nid=0x2b04 waiting for monitor entry
java.lang.Thread.State: BLOCKED (on object monitor)
at com.acme.billing.Ledger.credit(Ledger.java:88)
- waiting to lock <0x000000076ab41180> (a com.acme.billing.Account)
- locked <0x000000076ab3f2c8> (a com.acme.billing.Account)
at com.acme.billing.Ledger.transfer(Ledger.java:61)
Определите причину.
Оба потока BLOCKED на мониторе, и адреса мониторов перекрещены: worker-1 держит 0x…41180 и ждёт 0x…3f2c8, а worker-2 держит 0x…3f2c8 и ждёт 0x…41180. Это циклическое ожидание на двух мониторах Account — deadlock: transfer захватывает счета в порядке аргументов вызывающего, поэтому два встречных перевода берут их в противоположном порядке. Лечится единым глобальным порядком захвата (например, по id счёта) или tryLock с таймаутом и отступом.
- ✗Читать
BLOCKEDкак нехватку CPU или задержку планировщика, а не ожидание монитора - ✗Игнорировать адреса мониторов, которые и доказывают цикличность ожидания
- ✗Предлагать больший пул или высокий приоритет, что лишь добавит застрявших потоков
- →Какая строка
jstackподтвердила бы диагноз автоматически, без чтения кадров? - →Как переход с мониторов
AccountнаtryLockс таймаутом изменил бы этот дамп?
Разбор
Дамп даёт всё, что нужно, в четырёх строках. У каждого потока есть пара locked / waiting to lock:
worker-1: locked <0x…41180> waiting to lock <0x…3f2c8>
worker-2: locked <0x…3f2c8> waiting to lock <0x…41180>
Адреса перекрещены: монитор, который держит один поток, — ровно тот, которого ждёт второй, и наоборот. Оба в состоянии BLOCKED (on object monitor), то есть ни один не отпустит своё, пока не получит чужое. Это замкнутый цикл ожидания — deadlock. CPU при этом простаивает: заблокированный на мониторе поток не крутится, он спит.
Откуда взялся цикл. transfer(from, to) синхронизируется сначала на from, потом на to — то есть в порядке аргументов вызывающего. Два встречных перевода (A→B и B→A) захватывают одни и те же два Account в противоположном порядке, и цикл замыкается.
// ❌ порядок захвата задаёт вызывающий
void transfer(Account from, Account to, long amount) {
synchronized (from) {
synchronized (to) { from.debit(amount); to.credit(amount); }
}
}
// ✅ единый глобальный порядок — цикл не может сформироваться
void transfer(Account from, Account to, long amount) {
Account first = from.id() < to.id() ? from : to;
Account second = from.id() < to.id() ? to : from;
synchronized (first) {
synchronized (second) { from.debit(amount); to.credit(amount); }
}
}
Альтернатива, когда единый порядок задать нельзя, — ReentrantLock.tryLock с таймаутом: поток, не получивший вторую блокировку, отпускает первую и повторяет попытку со случайной задержкой.
⚠️ Ловушка: увеличение пула или приоритета потоков не помогает — новые потоки просто встанут в ту же очередь за теми же мониторами. jstack для этого случая печатает ещё и готовую строку Found one Java-level deadlock: со списком участников цикла.