MA-JAVA0 · Module 24
Waiting, interrupting, and deadlock
Topics: wait/notifyAll · InterruptedException · volatile · Deadlock
Overview
Session 24 — Waiting, interrupting, and deadlock
Locks stop races, but clerks still need to wait when the roll of tickets is empty and wake when a new batch arrives. This session covers wait / notifyAll on a monitor, InterruptedException as a cooperative stop signal, volatile for simple flags, and deadlock—two locks taken in opposite order. The goal is coordination you can trace on paper, not every library in java.util.concurrent.
Time: about 95 minutes.
Concepts
1. When exclusion is not enough
A clerk who finds remaining == 0 should not spin in a while loop burning a core. They should wait until a supervisor restocks, then try again. Waiting is a condition: “there are seats,” not merely “I hold the lock.”
Java’s intrinsic monitor gives you wait, notify, and notifyAll. They are easy to misuse. The rules below are the whole game for this session.
2. wait releases the lock
You may call obj.wait() only while holding obj’s monitor (synchronized (obj)). wait:
- releases that monitor (so others can restock);
- parks the thread;
- later re-acquires the monitor before
waitreturns.
If wait did not release the lock, the restocker could never enter synchronized (obj) and the booth would freeze.
Always wait in a loop:
synchronized (gate) {
while (remaining == 0 && open) {
gate.wait();
}
// re-check remaining — a wakeup is not a promise
}
A loop is required because of spurious wakeups and because another clerk may take the last seat between notify and your re-acquire.
3. notify versus notifyAll
notify() wakes one waiter (unspecified which). notifyAll() wakes all waiters; they then compete for the lock, one at a time.
If waiters wait for different conditions on the same lock (“seats available” vs “booth closed”), notify() can wake the wrong thread. notifyAll is the beginner-safe default. Optimise to notify only when every waiter waits for the same condition and one wakeup is enough.
Call notify while still holding the monitor, typically after you change the condition (remaining += batch).
4. Interrupts: a polite stop
Thread.interrupt() sets a flag on the target. Blocking calls (sleep, join, wait) throw InterruptedException and clear the flag.
Idiom after catching it:
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
Restoring the flag lets callers above you still see that shutdown was requested. Swallowing the exception with an empty catch hides cancellation.
Thread.interrupted() (static) reads and clears the flag. isInterrupted() (instance) reads without clearing. Use the instance method when you only want to look.
5. volatile flags
A boolean shutdown written by main and read by clerks can be volatile so the write becomes visible without a lock. That is appropriate for a single independent flag.
Do not use volatile as a replacement for synchronized around remaining++. Do not mix “sometimes lock, sometimes only volatile” on the same multi-step invariant.
6. Deadlock
Deadlock is a permanent wait cycle. Classic sketch:
- Thread A locks
drawer, then tries to lockregister. - Thread B locks
register, then tries to lockdrawer.
Each holds what the other needs. The JVM will not break this for you.
Defences you can actually apply:
- One lock per invariant when possible.
- If two locks are required, always acquire in the same global order (e.g.
drawerthenregistereverywhere). - Keep critical sections short so you are less tempted to take a second lock inside the first.
- Timeouts (
tryLockonjava.util.concurrent.locks.Lock) belong in later study; intrinsicsynchronizedhas no timeout.
7. Common confusions
“wait() on this from a thread that is not synchronized.” IllegalMonitorStateException. The error means you did not own the monitor.
“notify before wait.” If the restocker notifies when no clerk is waiting, the signal is lost. The waiting loop plus a stored condition (remaining > 0) is how you survive that: the clerk checks the condition first and may not wait at all.
“Deadlock is the same as a race.” A race produces wrong data. Deadlock produces no progress. You can have both in one program.
8. What you should be able to do
Park a clerk with wait in a while loop, wake with notifyAll after restock, restore interrupt status, and draw a two-lock deadlock. Next session: let a pool own the threads.
Examples
Example A — restock wakes waiters
public class RestockWindow {
private int remaining = 0;
private boolean open = true;
private final Object gate = new Object();
public void buyWhenAvailable() throws InterruptedException {
synchronized (gate) {
while (remaining == 0 && open) {
gate.wait();
}
if (!open) {
return;
}
remaining--;
}
}
public void restock(int batch) {
synchronized (gate) {
remaining += batch;
gate.notifyAll();
}
}
public void closeBooth() {
synchronized (gate) {
open = false;
gate.notifyAll();
}
}
}
Clerks call buyWhenAvailable. If the roll is empty they wait. A supervisor calls restock(8) and notifyAll. Closing the booth sets open and wakes everyone so nobody waits forever.
The while loop is not optional: after wakeup you re-check remaining and open.
Example B — restore interrupt status
public static void napQuietly() {
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
After the catch, isInterrupted() is true again. A later sleep will throw promptly. That is how cancellation propagates.
Example C — deadlock sketch (do not ship)
Object drawer = new Object();
Object register = new Object();
Thread a = new Thread(() -> {
synchronized (drawer) {
synchronized (register) {
System.out.println("a booked");
}
}
});
Thread b = new Thread(() -> {
synchronized (register) {
synchronized (drawer) {
System.out.println("b booked");
}
}
});
Run it in a loop in a throwaway class if you want to see a hang. The repair is the same lock order in both threads: drawer, then register.
Example D — volatile shutdown flag
public class FlagStop {
private static volatile boolean stop;
public static void main(String[] args) throws InterruptedException {
Thread ticker = new Thread(() -> {
while (!stop) {
// spin-wait is still a poor production pattern;
// the flag only solves visibility here
}
System.out.println("ticker saw stop");
});
ticker.start();
Thread.sleep(50);
stop = true;
ticker.join();
}
}
Without volatile, ticker may never observe stop = true on some JVMs. With volatile, the write is visible. This is not how you protect remaining++.
Practice check — wait, interrupt, deadlock
Multiple-choice check from the Matheion app lesson. Pick an answer for each question, then submit.
1.Calling `obj.wait()` requires that the current thread…
2.Wait for a condition with…
3.After catching `InterruptedException` from `sleep` or `wait`, a careful method often…
4.Deadlock in the two-lock sketch happens when…
5.`notify()` vs `notifyAll()`: a safe default while learning is…