🔴 The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
// DOC-DERIVED — canonical jstack/jcmd output shape; ids, addresses and frames vary per run
Found one Java-level deadlock:
=============================
"Thread-B":
waiting to lock monitor 0x00007fa1bc006b00 (object 0x000000076ab62208, a java.lang.Object),
which is held by "Thread-A"
"Thread-A":
waiting to lock monitor 0x00007fa1bc005e00 (object 0x000000076ab62238, a java.lang.Object),
which is held by "Thread-B"
Java stack information for the threads listed above:
===================================================
"Thread-B":
at com.devinhyderabad.billing.Account.transferFrom(Account.java:41)
at com.devinhyderabad.billing.TransferService.run(TransferService.java:22)
"Thread-A":
at com.devinhyderabad.billing.Account.transferFrom(Account.java:41)
at com.devinhyderabad.billing.TransferService.run(TransferService.java:25)
Found 1 deadlock.⚡ Quick Fix Works 80% of the time
Impose one global acquisition order for every pair of locks — sort by a stable key such as account id before locking, and the cycle becomes impossible.
// transfer(a, b): always lock the lower-id account first
Account first = a.id < b.id ? a : b;
Account second = a.id < b.id ? b : a;
synchronized (first) {
synchronized (second) {
debit(first, amount);
credit(second, amount);
}
}🧠 Why this Happens
Tap to expand the deep technical explanation
The JVM’s deadlock detector builds a wait-for graph over monitors and ownable synchronizers: each node is a thread, each edge points at the owner of the resource it blocks on. A cycle means no member can ever be scheduled to release — the runtime prints the participating threads, the objects, and the holding relationships verbatim. Detection is passive: it breaks no locks, interrupts nothing, and the threads remain frozen until restart or external intervention.
The HITEC City Parking Spot Analogy:
Two cars meeting head-on in an alley too narrow to pass: each waits for the other to reverse, forever. The traffic report (thread dump) names both drivers and the alley — but tow trucks are not dispatched automatically.
🔁 How to Reproduce Confirm this is your error
Thread A locks obj1 then sleeps then locks obj2; Thread B does the reverse. Run jcmd <pid> Thread.print (or jstack -l <pid>) once both park: the section above appears with your frames. (Canonical wording; not a lab capture.)
🛠️ Solutions (5 Ways to Fix)
Enforce global lock ordering by stable key
👉 Use this when/if two or more resources are locked together in varying orders.
Deadlock requires a cycle; a total order makes cycles structurally impossible because every thread contends in the same direction. Choose keys immune to churn — database ids, not names or array indices — and centralize the comparison so no call site improvises.
private static int orderKey(Account a) { return Long.compare(a.id, otherId); }
void transfer(Account from, Account to, Money amt) {
Account lower = from.id < to.id ? from : to;
Account upper = from.id < to.id ? to : from;
synchronized (lower) {
synchronized (upper) {
move(lower, upper, amt);
}
}
}Break cycles with tryLock and backoff
👉 Use this when/if ordering is impractical across legacy components.
ReentrantLock.tryLock attempts acquisition without parking; on failure, release everything held and retry after jitter. Threads may collide repeatedly but never freeze permanently — worst case degrades into bounded retry cost rather than a wedged server requiring restart.
if (a.lock.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
if (b.lock.tryLock(50, TimeUnit.MILLISECONDS)) {
try { move(a, b, amt); return true; }
finally { b.lock.unlock(); }
}
} finally { a.lock.unlock(); }
}
sleepWithJitter(20); // backoff, then whole sequence retriedShrink critical sections and ban alien calls under lock
👉 Use this when/if locks are held across callback or remote invocations.
Every method invoked while holding a lock can secretly acquire more locks — listener callbacks, ORM flushes, cache loaders. Copy needed data out under the lock, release, then do alien work lock-free. Smaller critical sections also shrink contention windows regardless of deadlocks.
Snapshot s;
synchronized (account) {
s = account.snapshot(); // copy primitives out
}
auditService.record(s); // alien call outside the lockReplace paired-lock patterns with concurrent structures
👉 Use this when/if the deadlock guards a shared map or counter pair.
ConcurrentHashMap.compute atomizes per-key updates under one internal striping scheme, and LongAdder replaces lock-paired counters. Deleting the second lock deletes the cycle; the JDK authors already debugged the concurrency so you delete the bug class wholesale.
balances.compute(from, (id, bal) -> bal.subtract(amt));
balances.compute(to, (id, bal) -> bal.add(amt));
// no explicit locks anywhere in transfer()Deploy continuous detection: watchdog plus scheduled dumps
👉 Use this when/if you must catch regressions before customers notice a frozen feature.
A background probe polls ThreadMXBean.findDeadlockedThreads every few seconds; on discovery it captures jcmd Thread.print output, alerts, and optionally flags the offending transactions. Deadlocks then page an engineer with evidence attached instead of surfacing as mysterious stalled traffic.
ThreadMXBean mx = ManagementFactory.getThreadMXBean();
long[] stuck = mx.findDeadlockedThreads();
if (stuck != null) {
dumpAndAlert(mx.getThreadInfo(stuck, /*maxDepth*/ 60));
}📋 Version Notes
jstack -l prints this exact section; jcmd <pid> Thread.print equally available.
Output unchanged; jcmd preferred tooling-wise.
Virtual threads appear in dumps grouped separately from carrier platform threads; verify pinned-monitor cases with JFR events alongside traditional detection.
🛡️ How to Prevent This Next Time
Document a lock-ordering convention per subsystem, gate new nested-lock code through review, run stress tests with randomized interleavings (JCStress-style or simply high-concurrency soak tests) so cycles surface in CI, and keep the watchdog enabled in production from day one.