↓ Skip to main content

Java Concurrency: Thread Communication

·566 words·3 mins

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)
wait / notifyAll handoffThe lock changes hands twice
  • You must hold the monitor to call wait or notify, or you get IllegalMonitorStateException.
  • 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. If markReady() runs before anyone waits, a bare wait() sleeps forever (the lost wakeup). The while (!ready) check sees true and 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 to notifyAll().

Modern code mostly uses BlockingQueue, CountDownLatch or Condition, all built on this same pattern.

join, sleep, yield
#

CallEffectReleases locks
t.join()waits until t terminatesn/a
Thread.sleep(ms)pauses the current threadno, keeps all monitors
lock.wait(ms)pauses and gives up the monitoryes, that one monitor
Thread.yield()scheduler hint, mostly uselessno

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() or lockInterruptibly(): it throws InterruptedException and clears the flag.
  • Running normal code: nothing, until the code checks Thread.currentThread().isInterrupted().
  • Waiting to enter synchronized, or reading from a classic java.io socket: 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.

Amar Singh
Author
Amar Singh
I build stuff

Related