ExerciseWarm-up
Warm-up
Deadlock two threads and detect it
10 minjunior0–15 yrs
One concept, guided. Near-impossible to fail.
What this teaches
- A deadlock is a cycle in the waits-for graph, and the JVM can prove one exists
- Forcing the interleaving with a latch is what makes the bug reproducible
- A global lock order removes the possibility rather than recovering from it
- A thread blocked entering a monitor cannot be interrupted, timed out or rescued
Starter
Starter.java
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
/**
* Warm-up: cause a deadlock on purpose, then let the JVM prove it.
*
* Two rules make this safe to run repeatedly:
*
* - every thread you start here must be a daemon, so a permanently stuck one
* cannot stop the JVM exiting;
* - never join without a timeout.
*
* Both are habits worth keeping when writing any concurrency test.
*/
public class Starter {
record Account(String id) {}
static Thread daemon(String name, Runnable body) {
var t = new Thread(body, name);
t.setDaemon(true);
t.start();
return t;
}
public static void main(String[] args) throws Exception {
var a = new Account("account-A");
var b = new Account("account-B");
// TODO 1: start two daemon threads. One locks `a` then `b`; the other
// locks `b` then `a`. Run it a few times. Does it hang every time?
// TODO 2: make it hang EVERY time. Use a CountDownLatch(2): inside the
// first synchronized block, countDown() and then await(). Now neither
// thread can reach for its second lock until both hold their first.
//
// Say in one sentence why the latch does not change whether the bug
// exists — only whether you see it.
// TODO 3: ask the JVM to confirm it, from the main thread:
//
// ThreadMXBean mx = ManagementFactory.getThreadMXBean();
// long[] stuck = mx.findDeadlockedThreads(); // null if none
// for (ThreadInfo t : mx.getThreadInfo(stuck)) { ... }
//
// Print each thread's name, the lock it waits for, and the lock's
// owner. Poll in a short loop rather than sleeping a fixed time.
// TODO 4: the lock name prints as ClassName@hashcode, which is not
// readable. Build a Map from that string to your own name:
//
// o.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(o))
//
// and translate the output.
// TODO 5: now fix it. Before locking, order the two accounts by id and
// always take the lower one first. Use FRESH Account objects — the
// threads from TODO 2 still hold the old ones and always will.
// Confirm both transfers complete.
// TODO 6: count how many lines that fix took, and say what it did NOT
// require: no timeout, no retry, no detection, no coordination.
// TODO 7: try to rescue a deadlocked thread. Start a thread blocked on
// a synchronized block that will never be released, wait until its
// state is BLOCKED, then interrupt() it. Print getState() and
// isInterrupted() afterwards. Explain the combination you see.
// TODO 8: repeat TODO 7 with a ReentrantLock and lockInterruptibly().
// What is different, and what does that tell you about when the extra
// ceremony of ReentrantLock is worth paying for?
}
}Run it locally:
cd exercises/java/concurrency/deadlock/01-warmup
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterDone when
- You deadlocked two threads deliberately and it happened on every run
- findDeadlockedThreads named both threads and both locks
- Sorting the locks by a stable key made the same code complete
- You interrupted a thread blocked on synchronized and showed it stayed BLOCKED