What's the difference between wait/notify and Java's higher-level concurrency utilities?
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()): likewait/notifybut 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 behindput()/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.