Java Programs›Multithreading›Avoid Deadlock with Lock Ordering

Avoid Deadlock with Lock Ordering in Java

advanced·  Multithreading  ·  Concurrency

Problem

A deadlock happens when two threads each hold one lock and wait forever for the lock the other holds; acquiring locks in one global order makes that circular wait impossible.

Run thousands of transfers in both directions between two accounts at the same time without deadlocking or losing any money.

Input
10,000 transfers Alice→Bob and 10,000 Bob→Alice
Output
balances 1000 and 1000, total 2000

Java Program

Java
public class DeadlockLockOrdering { static class Account { final int id; int balance; Account(int id, int balance) { this.id = id; this.balance = balance; } } static void transfer(Account from, Account to, int amount) { Account first = from.id < to.id ? from : to; // always lock the lower id first, Account second = from.id < to.id ? to : from; // whichever direction the money moves synchronized (first) { synchronized (second) { from.balance -= amount; to.balance += amount; } } } public static void main(String[] args) throws InterruptedException { Account alice = new Account(1, 1000); Account bob = new Account(2, 1000); Thread t1 = new Thread(() -> { for (int i = 0; i < 10_000; i++) transfer(alice, bob, 1); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 10_000; i++) transfer(bob, alice, 1); // opposite direction }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Alice: " + alice.balance); System.out.println("Bob: " + bob.balance); System.out.println("Total: " + (alice.balance + bob.balance) + " (money is never lost)"); } }

Output

Alice: 1000 Bob: 1000 Total: 2000 (money is never lost)

Core Logic

The deadlock-prone version locks from then to, so opposite transfers grab the locks in opposite orders; ordering the locks by account id removes that possibility.

How It Works
  1. 1first is whichever account has the lower id, and second the other, regardless of transfer direction.
  2. 2Both threads therefore lock Alice (id 1) before Bob (id 2).
  3. 3A thread that holds Alice's lock can always go on to get Bob's, because no thread ever holds Bob's lock while waiting for Alice's.
  4. 4After both threads finish, the balances are printed; equal and opposite transfers leave them unchanged.
If thread 1 moves money Alice→Bob and thread 2 moves it Bob→Alice, both still start by locking Alice, so one simply waits for the other.
💡

Key Point: With synchronized (from) then synchronized (to), this program can freeze forever — thread 1 holding Alice and waiting for Bob, thread 2 holding Bob and waiting for Alice.

Key Concepts

deadlocklock orderingnested synchronizedjoin()

Approach 2: Using ReentrantLock.tryLock()

Java
import java.util.concurrent.locks.ReentrantLock; public class DeadlockTryLock { static class Account { final ReentrantLock lock = new ReentrantLock(); int balance; Account(int balance) { this.balance = balance; } } static void transfer(Account from, Account to, int amount) { while (true) { if (from.lock.tryLock()) { // never blocks: returns false if another thread holds the lock try { if (to.lock.tryLock()) { try { from.balance -= amount; to.balance += amount; return; } finally { to.lock.unlock(); } } } finally { from.lock.unlock(); // couldn't get both: release what we hold } } Thread.yield(); // back off so the other thread can finish its transfer, then retry } } public static void main(String[] args) throws InterruptedException { Account alice = new Account(1000); Account bob = new Account(1000); Thread t1 = new Thread(() -> { for (int i = 0; i < 1_000; i++) transfer(alice, bob, 1); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 1_000; i++) transfer(bob, alice, 1); // opposite direction }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Alice: " + alice.balance + ", Bob: " + bob.balance + ", Total: " + (alice.balance + bob.balance)); } }

Output

Alice: 1000, Bob: 1000, Total: 2000

Core Logic

Instead of waiting forever, tryLock() gives up immediately if a lock is taken; the thread releases what it holds and tries again later.

How It Works
  1. 1Each account has its own ReentrantLock.
  2. 2from.lock.tryLock() and to.lock.tryLock() return false instead of blocking when the lock is busy.
  3. 3If the second lock can't be acquired, the finally block releases the first, so no thread ever waits while holding a lock.
  4. 4Thread.yield() backs off briefly before the next attempt, giving the other thread a chance to finish.
When both threads grab their first lock at the same moment, both fail on the second, release, and retry — one of them soon gets both locks.
💡

Key Point: tryLock avoids deadlock but can cause livelock, where threads keep retrying in step; a back-off such as yield() or a short random sleep breaks the symmetry.

Key Concepts

ReentrantLocktryLock()back-off and retry

Interview Questions

What four conditions are needed for a deadlock?

Mutual exclusion, hold and wait, no preemption, and a circular wait. Lock ordering prevents the circular wait, which is enough to stop deadlock.

How can you detect a deadlock in a running program?

A thread dump (from jstack or a profiler) lists threads blocked on each other's locks and usually reports "Found one Java-level deadlock".

Learn the Concept

Related Programs