Blog Concurrency and Multithreading Java

Mastering Java Concurrency with wait() and notify()

Introduction

In multithreaded Java applications, managing the communication between threads is crucial. Two of the fundamental methods that facilitate this are wait() and notify(). This post will explore these methods and how they enable smooth thread synchronization.

The Role of wait() and notify() in Synchronization

Begin with an explanation of the importance of thread synchronization and where wait() and notify() fit into this picture. Discuss the concept of the monitor lock and how these methods manage it to ensure threads communicate efficiently without running into race conditions or deadlocks.

Understanding wait()

Explain that wait() is a method that causes the current thread to wait until another thread invokes the notify() method for the same object.

Key Points:

  • It must be called within a synchronized context.
  • The thread releases the lock and waits until it’s notified or interrupted.
  • Overloads of wait() allow specifying a timeout after which the thread will wake up if not notified.

Understanding notify() and notifyAll()

Discuss how notify() wakes up a single waiting thread, while notifyAll() wakes up all the threads that are waiting on that object’s monitor.

Key Points:

  • Only one thread is chosen to be awakened by notify(). The choice is arbitrary and left to the JVM.
  • notifyAll() allows multiple threads to wake up and compete for the lock, but only one will proceed as they must acquire the lock to continue execution.

Java Example

Following sample code that demonstrates the use of wait() and notify().

public class SharedResource {

    private boolean isResourceAvailable;

    // This method could be called by other methods to update the condition
    public synchronized void makeResourceAvailable() {
        isResourceAvailable = true;
        notifyAll(); // Notify all waiting threads that the condition has changed
    }

    public synchronized void consumeResource() {
        isResourceAvailable = false; // Resource is not available after consuming
    }

    // Dummy condition method
    private boolean someCondition() {
        return isResourceAvailable;
    }

    public synchronized void waitForResource() {
        while (!someCondition()) {
            try {
                System.out.println(Thread.currentThread().getName() + " is waiting for the resource.");
                wait();
                System.out.println(Thread.currentThread().getName() + " has been notified.");
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " was interrupted.");
            }
        }
        // Resource is now available to consume
        consumeResource();
        System.out.println(Thread.currentThread().getName() + " has consumed the resource.");
    }

    public synchronized void releaseResource() {
        System.out.println("Resource is released.");
        makeResourceAvailable();
        notifyAll();
    }
}
}

// This class would be used to create and run threads that use the SharedResource
public class SharedResourceTest {
    public static void main(String[] args) {
        SharedResource sharedResource = new SharedResource();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                sharedResource.waitForResource();
            }
        }, "Thread 1");

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                sharedResource.releaseResource();
            }
        }, "Thread 2");

        t1.start();
        try {
            Thread.sleep(1000); // Ensure Thread 1 starts waiting before Thread 2 releases the resource
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

Code Explanation

Spurious Wakeups

Spurious wakeups are a well-known phenomenon where a thread might wake up from waiting without being notified, interrupted, or timing out. The Java language specification does not rule out spurious wakeups, thus it’s possible for wait() to return without notify() or notifyAll() having been called. This is why you should always call wait() within a loop, as the loop will recheck the condition upon wakeup and continue waiting if the condition is not met.

Condition Checks

In complex systems, you may have multiple conditions that could wake up a thread. Using a loop allows you to recheck the specific condition you are interested in before proceeding. There might also be multiple threads waiting on the same object, and a notify() call might wake up a thread for which the condition it’s waiting for has not yet been satisfied.

Here is an example to illustrate the use of while (!someCondition()) with wait():

javaCopy code

public synchronized void waitForResource() {
    while (!someCondition()) {
        try {
            wait();  // Release lock and wait
        } catch (InterruptedException e) {
            // Handle interruption
        }
    }
    // Proceed with the assumption that someCondition() is true
}

In this example, even if a thread is woken up from the wait state (due to a spurious wakeup or another thread’s notify() call), it will not proceed until someCondition() evaluates to true. If the condition is not true, it will go back into waiting by calling wait() again within the loop.

Without the loop, if the thread were woken up due to a spurious wakeup and the condition was not actually met, it would proceed as if it was, potentially leading to incorrect behavior.

Including notifyAll() at the end of releaseResource() ensures that all threads waiting on the SharedResource object’s monitor are notified. Once notified, Thread 1 will re-check the condition in the waitForResource() method. Since isResourceAvailable is now true, Thread 1 will exit the while loop, consume the resource, and print the message indicating that it has consumed the resource.

Best Practices

Offer some best practices for using wait() and notify(), such as always using them within a loop to avoid spurious wakeups, handling InterruptedException, and ensuring proper notification logic to prevent lost signals.

Common Pitfalls

Address common mistakes and pitfalls, such as deadlocks, not calling within synchronized code, and forgetting to call notify() or notifyAll(), leading to threads that wait forever.

Why Call wait() and notify() Within a Synchronized Block?

  1. Monitor Ownership: A thread must own the monitor of the object on which it’s calling wait() or notify(). This ownership is only possible if the call is made from within a synchronized context.
  2. Atomicity: The actions of calling wait() or notify() should be atomic with respect to other operations on the shared resource. Synchronization ensures that the state of the resource is consistent and not altered by another thread in the middle of a wait-notify sequence.
  3. IllegalMonitorStateException: Calling wait() or notify() without holding the lock on the object’s monitor will result in an IllegalMonitorStateException.

Code Example – Incorrect Usage:

public class SharedResource {
    // Assume this is a shared resource
    public void doWait() {
        try {
            wait(); // This will throw IllegalMonitorStateException
        } catch (InterruptedException e) {
            // Handle interruption
        }
    }

    public void doNotify() {
        notify(); // This will throw IllegalMonitorStateException
    }
}

Running the methods doWait() or doNotify() will result in an IllegalMonitorStateException because they are not synchronized and the current thread does not hold the monitor’s lock.

Alternatives to wait() and notify()

Briefly mention modern alternatives like the java.util.concurrent package, which provides higher-level constructs like ReentrantLock, Condition, Semaphore, etc., that can be used for thread synchronization with more control and flexibility.

Conclusion

Summarize the importance of understanding and correctly implementing wait() and notify() for effective thread communication and synchronization in Java applications.

Call to Action

Encourage readers to experiment with these methods in their code, keeping in mind the best practices and common pitfalls. Invite them to share their experiences or questions in the comments section.

This structure provides a comprehensive overview of wait() and notify(), suitable for both beginners and experienced Java developers looking to deepen their understanding of concurrency.

Avatar

Neelabh

About Author

As Neelabh Singh, I am a Senior Software Engineer with 6.6 years of experience, specializing in Java technologies, Microservices, AWS, Algorithms, and Data Structures. I am also a technology blogger and an active participant in several online coding communities.

You may also like

Blog Design Pattern

Understanding the Builder Design Pattern in Java | Creational Design Patterns | CodeTechSummit

Overview The Builder design pattern is a creational pattern used to construct a complex object step by step. It separates
Blog Tech Toolkit

Base64 Decode

Base64 encoding is a technique used to encode binary data into ASCII characters, making it easier to transmit data over