The classic deadlock is two threads acquiring the same two locks in opposite order: thread 1 takes A then wants B, thread 2 takes B then wants A. Each holds what the other needs, so both block forever.
The code
public class TwoLockDeadlock {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (LOCK_A) {
System.out.println("T1 holds A, wants B");
sleep(50); // give T2 time to grab B
synchronized (LOCK_B) { // blocks: T2 holds B
System.out.println("T1 got both");
}
}
}, "T1");
Thread t2 = new Thread(() -> {
synchronized (LOCK_B) { // opposite order!
System.out.println("T2 holds B, wants A");
sleep(50);
synchronized (LOCK_A) { // blocks: T1 holds A
System.out.println("T2 got both");
}
}
}, "T2");
t1.start();
t2.start();
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
Why it deadlocks
The sleep(50) is not the cause — it just makes the race deterministic. The real cause is inconsistent lock ordering: T1 acquires A → B, T2 acquires B → A. Once T1 owns A and T2 owns B, T1's request for B and T2's request for A can never be satisfied. All four Coffman conditions hold: the monitors are mutually exclusive, each thread holds one while waiting for the other, the JVM never preempts a monitor, and the wait graph is a cycle.
This pattern hides everywhere in real systems: a bank transfer(from, to) where one thread moves A→B while another moves B→A; two services each locking a shared cache and a DB row in different orders; nested synchronized collections.
The fix is to impose a global order so every thread takes the lower-ranked lock first (e.g. by System.identityHashCode), making circular wait impossible.