Prevent deadlock by breaking one of the four Coffman conditions — most often circular wait, via a consistent global lock-ordering. Where you cannot order locks, use timed tryLock to break hold-and-wait, and shrink critical sections so locks are held briefly.
1. Lock ordering (kills circular wait)
If every thread acquires locks in the same total order, no cycle can form. Derive a stable rank — a business ID, or System.identityHashCode as a fallback:
void transfer(Account from, Account to, long amount) {
Account first = from.id() < to.id() ? from : to;
Account second = first == from ? to : from;
synchronized (first.lock()) {
synchronized (second.lock()) {
from.debit(amount);
to.credit(amount);
}
}
}
Handle the from.id() == to.id() self-transfer case (use a tie-breaker or reject it) so you do not deadlock on yourself.
2. tryLock with timeout (kills hold-and-wait)
When a global order is impractical, acquire locks optimistically and back off if you cannot get them all:
boolean transfer(Account a, Account b, long amount) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(200);
while (System.nanoTime() < deadline) {
if (a.lock().tryLock(50, TimeUnit.MILLISECONDS)) {
try {
if (b.lock().tryLock(50, TimeUnit.MILLISECONDS)) {
try { a.debit(amount); b.credit(amount); return true; }
finally { b.lock().unlock(); }
}
} finally { a.lock().unlock(); }
}
Thread.sleep(ThreadLocalRandom.current().nextInt(20)); // randomized backoff avoids livelock
}
return false;
}
The randomized backoff matters — fixed backoff turns a deadlock into a livelock where both threads retry in lockstep.
3. Open calls and smaller critical sections
An open call means invoking external/alien code (callbacks, other components) without holding a lock. Copy the data you need, release the lock, then make the call — alien code might try to grab a lock in the wrong order. Generally, hold locks for the shortest possible span and never block on I/O or await while holding one.