What's the difference between wait/notify and Java's higher-level concurrency utilities?

9 minadvancedwait-notifyjava.util.concurrentcoordination

Quick Answer

wait()/notify()/notifyAll() are low-level Object methods for thread coordination that must be called inside a synchronized block on the same monitor, are easy to misuse (missed signals, spurious wakeups requiring a while-loop check), and offer no fairness or timeout conveniences beyond a bare millisecond parameter. Higher-level utilities in java.util.concurrent (Lock/Condition, CountDownLatch, Semaphore, CompletableFuture, BlockingQueue) provide the same coordination with clearer APIs, better composability, and fewer footguns.

Detailed Answer

wait()/notify()/notifyAll() (inherited from Object) are the original, low-level thread coordination primitives, tied directly to intrinsic locks:

synchronized (lock) {
    while (!conditionMet) {
        lock.wait(); // must be in a while-loop, not if — spurious wakeups are possible
    }
    // proceed
}

// elsewhere
synchronized (lock) {
    conditionMet = true;
    lock.notifyAll(); // notify(), not notifyAll(), risks waking the "wrong" waiter and losing a signal
}

Pitfalls that make this error-prone: you must call wait()/notify() while holding the object's monitor (IllegalMonitorStateException otherwise), a notify() can wake an arbitrary one of several waiting threads (use notifyAll() unless you're certain only one kind of waiter exists), spurious wakeups mean the condition must be rechecked in a loop, and signals sent before a thread starts waiting are simply lost (no queuing).

java.util.concurrent (since Java 5) provides higher-level, more composable alternatives for the same problems:

  • Lock/Condition (ReentrantLock.newCondition()): like wait/notify but supports multiple independent condition queues per lock, timed waits, and interruptible/tryLock acquisition.
  • CountDownLatch/CyclicBarrier/Semaphore: purpose-built for common coordination patterns (wait for N tasks, rendezvous, limit concurrent access) without hand-rolling wait/notify logic.
  • BlockingQueue: encapsulates the entire producer-consumer wait/notify dance behind put()/take().
  • CompletableFuture: composable async pipelines, replacing manual thread coordination with declarative .thenApply()/.thenCombine() chains.

Practical guidance: reach for java.util.concurrent utilities first; hand-written wait/notify is now mostly relevant for understanding fundamentals and for rare cases needing that exact low-level control.