Java tutorials > Multithreading and Concurrency > Threads and Synchronization > Difference between `wait()`, `notify()`, `notifyAll()`?

Difference between `wait()`, `notify()`, `notifyAll()`?

In Java multithreading, wait(), notify(), and notifyAll() are essential methods for thread synchronization and communication. They enable threads to coordinate their actions by pausing execution (waiting) and signaling other threads (notifying) when certain conditions are met. These methods are part of the Object class and are used in conjunction with synchronized blocks or methods to prevent race conditions and ensure data consistency.

Core Concepts Behind `wait()`, `notify()`, and `notifyAll()`

These methods operate within a synchronized context (a synchronized block or method). They facilitate inter-thread communication by allowing threads to temporarily release the lock on an object (wait()) and reacquire it when notified by another thread (notify() or notifyAll()).

  • wait(): Causes the current thread to release the lock on the object and enter a waiting state until another thread invokes notify() or notifyAll() for that object.
  • notify(): Wakes up a single thread that is waiting on the object's monitor. If multiple threads are waiting, the JVM chooses one arbitrarily.
  • notifyAll(): Wakes up all threads that are waiting on the object's monitor. Each thread then competes to reacquire the lock.

It's crucial to call these methods from within a synchronized block or method; otherwise, an IllegalMonitorStateException will be thrown.

Basic Usage Example

This example illustrates a simple producer-consumer scenario using a DataBuffer. The write() method, used by the producer, waits if the buffer is full and then writes data and notifies the consumer. The read() method, used by the consumer, waits if the buffer is empty, reads data and notifies the producer. Both methods are synchronized to protect shared resources.

class DataBuffer {
    private String data;
    private boolean isEmpty = true;

    public synchronized void write(String newData) throws InterruptedException {
        while (!isEmpty) {
            wait(); // Wait if the buffer is not empty
        }
        data = newData;
        isEmpty = false;
        System.out.println("Written: " + data);
        notifyAll(); // Notify waiting threads that data is available
    }

    public synchronized String read() throws InterruptedException {
        while (isEmpty) {
            wait(); // Wait if the buffer is empty
        }
        String result = data;
        isEmpty = true;
        System.out.println("Read: " + result);
        notifyAll(); // Notify waiting threads that the buffer is empty
        return result;
    }
}

public class WaitNotifyExample {
    public static void main(String[] args) {
        DataBuffer buffer = new DataBuffer();

        Thread writer = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    buffer.write("Data " + i);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread reader = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    buffer.read();
                    Thread.sleep(150);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        writer.start();
        reader.start();
    }
}

Real-Life Use Case: Message Queues

Message queues are a practical example. Producers place messages in a queue, and consumers retrieve messages from the queue. wait() and notify()/notifyAll() are used to signal when the queue has new messages (for consumers) or available space (for producers). This enables asynchronous communication between different parts of a system.

When to use `wait()`, `notify()`, and `notifyAll()`

Use these methods when you need threads to coordinate their actions based on specific conditions. Specifically:

  • When a thread needs to wait for a condition to become true before proceeding.
  • When one thread needs to signal another thread that a certain condition has been met.
  • In producer-consumer scenarios, resource management, or any situation where threads must synchronize access to shared resources.

Difference between `notify()` and `notifyAll()` in detail

The key difference lies in which waiting threads are woken up:

  • notify(): Wakes up only one arbitrary thread waiting on the object's monitor. If multiple threads are waiting, there's no guarantee which one will be chosen. This can lead to 'spurious wakeups' where a thread wakes up but the condition it was waiting for is still not true.
  • notifyAll(): Wakes up all threads waiting on the object's monitor. Each thread then contends for the lock, and after acquiring it, checks the condition to see if it can proceed.

notifyAll() is generally preferred because it avoids the risk of a single thread being repeatedly notified while others remain waiting indefinitely ('lost wakeup' problem). However, notifyAll() can be less efficient if many threads are waiting, as they all wake up and compete for the lock, even if only one can proceed.

Best Practices

  • Always use wait() in a loop: Conditions can change between the time a thread is notified and the time it reacquires the lock. Therefore, always re-check the condition in a loop. This handles spurious wakeups.
  • Use notifyAll() unless you have a very specific reason to use notify(): notifyAll() generally leads to more robust code.
  • Minimize the time spent holding the lock: Release the lock as soon as possible after performing the necessary operations to avoid blocking other threads.
  • Avoid deadlocks: Ensure that threads acquire locks in a consistent order to prevent deadlocks.

Memory Footprint

The memory footprint of these methods is relatively small. Each object maintains a wait set (a queue of waiting threads). The size of this wait set depends on the number of threads that are waiting on the object's monitor. The overhead is generally negligible unless you have a very large number of threads waiting on a single object.

Alternatives

Alternatives to wait(), notify(), and notifyAll() include:

  • java.util.concurrent package: This package provides higher-level concurrency utilities like Lock, Condition, BlockingQueue, and Semaphore, which can simplify complex synchronization scenarios.
  • CountDownLatch: Allows one or more threads to wait until a set of operations being performed in other threads completes.
  • CyclicBarrier: Allows a set of threads to all wait for each other to reach a common barrier point.
  • Exchanger: Allows two threads to exchange objects.

Using these higher-level constructs often leads to cleaner and more maintainable code.

Interview Tip

When discussing these methods in an interview, be sure to explain the following:

  • The purpose of each method.
  • The importance of using them within synchronized blocks or methods.
  • The difference between notify() and notifyAll(), including the advantages and disadvantages of each.
  • The possibility of spurious wakeups and how to handle them.
  • Potential alternatives using the java.util.concurrent package.

Pros of Using `wait()`, `notify()`, and `notifyAll()`

  • Fine-grained Control: They provide precise control over thread synchronization.
  • Fundamental: Understanding these methods is fundamental to understanding Java's concurrency model.
  • Low-level: They are built-in and don't require external libraries.

Cons of Using `wait()`, `notify()`, and `notifyAll()`

  • Error-prone: They are low-level and can be difficult to use correctly, leading to deadlocks and other concurrency issues.
  • Complexity: Managing thread synchronization with these methods can become complex in large applications.
  • Alternatives Exist: Higher-level concurrency utilities often provide a more convenient and safer way to achieve the same results.

FAQ

  • What happens if I call `wait()`, `notify()`, or `notifyAll()` outside of a synchronized block or method?

    An IllegalMonitorStateException will be thrown at runtime.
  • What is a spurious wakeup?

    A spurious wakeup is when a thread wakes up from a wait() call even though it has not been notified. This can happen due to reasons internal to the JVM. That's why you should always use `wait()` inside a loop that checks the condition you're waiting for.
  • Why is `notifyAll()` generally preferred over `notify()`?

    notifyAll() avoids the 'lost wakeup' problem, where a thread might miss a notification and wait indefinitely. While it can be less efficient in some cases, it's generally more robust.
  • How do I prevent deadlocks when using these methods?

    Ensure that threads acquire locks in a consistent order and avoid holding multiple locks at the same time whenever possible. Careful design of your synchronization logic is crucial.