Threads coordinate through a shared condition guarded by a lock. wait() releases the monitor and parks; notifyAll() wakes the waiters, and each one must take the monitor back and re-check the condition in a while loop. sleep() keeps every lock it holds, while wait() gives up its monitor. Cancellation in Java is cooperative: interrupt() only sets a flag, blocking calls turn it into InterruptedException, and code that swallows that exception is the usual reason an app hangs on shutdown.
Part of the Java Concurrency Roadmap. Builds on Synchronization basics.
wait and notifyAll#
void awaitReady() throws InterruptedException {
synchronized (lock) {
while (!ready) lock.wait(); // releases lock while parked
}
}
void markReady() {
synchronized (lock) {
ready = true; // change the condition first
lock.notifyAll();
}
}
sequenceDiagram participant A as Thread A (waiter) participant M as Monitor (lock) participant B as Thread B (signaller) A->>M: lock, check ready (false) A->>M: wait() releases lock B->>M: lock, set ready = true B->>M: notifyAll(), unlock M-->>A: wake up A->>M: reacquire lock, recheck ready (true)
- You must hold the monitor to call
waitornotify, or you getIllegalMonitorStateException. - Loop on the condition for two reasons. The JVM allows spurious wakeups, and another thread can grab the lock between the notify and your reacquire and flip the condition back.
- The condition is a field (
ready). Treating “a notify happened” as the condition breaks. IfmarkReady()runs before anyone waits, a barewait()sleeps forever (the lost wakeup). Thewhile (!ready)check seestrueand never waits. notify()wakes one arbitrary thread. When producers and consumers wait on the same monitor for different conditions, it can wake the wrong kind and stall everyone. Default tonotifyAll().
Modern code mostly uses BlockingQueue, CountDownLatch or Condition, all built on this same pattern.
join, sleep, yield#
| Call | Effect | Releases locks |
|---|---|---|
t.join() | waits until t terminates | n/a |
Thread.sleep(ms) | pauses the current thread | no, keeps all monitors |
lock.wait(ms) | pauses and gives up the monitor | yes, that one monitor |
Thread.yield() | scheduler hint, mostly useless | no |
If thread A holds lock and calls sleep(5000), every other thread that wants lock sits in BLOCKED for five seconds. If it calls lock.wait(5000) instead, they can take the lock right away. join() is also a happens-before edge, so after it returns you see every write the joined thread made. For spin loops, use Thread.onSpinWait() instead of yield().
Interrupts#
t.interrupt() sets a flag on t. What happens next depends on what t is doing:
- Blocked in
sleep,wait,join,queue.take()orlockInterruptibly(): it throwsInterruptedExceptionand clears the flag. - Running normal code: nothing, until the code checks
Thread.currentThread().isInterrupted(). - Waiting to enter
synchronized, or reading from a classicjava.iosocket: it stays stuck. NIO channels do respond, by closing the channel.
Thread.interrupted() checks and clears the flag. isInterrupted() only checks.
while (!Thread.currentThread().isInterrupted()) {
try {
queue.take().process();
} catch (InterruptedException e) {
log.warn("interrupted", e); // bug: flag was cleared, loop runs forever
}
}
take() cleared the flag when it threw, so the loop condition stays true and the worker goes straight back to take(). shutdownNow(), future.cancel(true) and Spring’s graceful shutdown all rely on interrupts, so this worker keeps the app alive until the shutdown timeout kills it. Either let the exception propagate, or restore the flag with Thread.currentThread().interrupt() and exit the loop.
Producer-consumer gets its own walkthrough in Classic problems.
