Lesson 4
Thread Coordination with wait/notify
Thread Coordination with wait() and notify()

Welcome back to our advanced Java concurrency course! In previous lessons, we explored lock mechanisms and deadlock prevention. Now, we will focus on thread coordination using the wait() and notify() methods in Java, a powerful approach to managing inter-thread communication.

What You'll Learn

In this lesson, you will:

  • Learn how to implement thread coordination using the wait() and notify() methods.
  • Understand how to synchronize threads for correct execution order.
  • Apply countdown logic to real-world scenarios involving thread dependencies.

By the end of this lesson, you’ll be able to use wait() and notify() to coordinate thread activities effectively, improving the performance and reliability of your concurrent applications.

Understanding Thread Coordination with wait() and notify()

Thread coordination involves managing the execution order of threads, ensuring that they wait for specific conditions before continuing their work. The wait() and notify() methods allow threads to communicate with each other and synchronize their actions.

In this example, we’ll use a countdown mechanism to coordinate two threads: one that waits for the countdown to finish, and another that performs the countdown and notifies the waiting thread when it’s complete.

Java
1public class Countdown { 2 private boolean isReady = false; 3 4 public synchronized void waitForCountdown() throws InterruptedException { 5 while (!isReady) { 6 System.out.println("Waiting for countdown..."); 7 wait(); 8 } 9 System.out.println("Countdown complete! Proceeding..."); 10 } 11 12 public synchronized void startCountdown() throws InterruptedException { 13 System.out.println("Countdown started..."); 14 Thread.sleep(2000); // Simulate countdown time 15 isReady = true; 16 notify(); 17 System.out.println("Countdown finished! Notified waiting thread."); 18 } 19}

In the Countdown class, we manage a boolean flag isReady to coordinate two actions: waiting for a countdown and performing the countdown. The wait() method causes the waiting thread to pause until the countdown completes, while notify() alerts the waiting thread that it can now proceed.

  • The wait() method makes the thread release the lock on the object and pauses its execution until another thread calls notify() or notifyAll(). In this case, the waitForCountdown() method uses wait() to make the thread wait while the countdown is ongoing.

  • The notify() method wakes up a single waiting thread. It signals that the countdown is complete, allowing the waiting thread to proceed with its task. In the startCountdown() method, notify() is called after the countdown has finished, allowing the waiting thread to resume execution.

By using a boolean flag (isReady), we ensure that the thread only proceeds after the countdown is finished. This mechanism, using wait() and notify(), is essential for coordinating thread actions without wasting resources by having threads check for a condition in a loop.

Both the waitForCountdown() and startCountdown() methods throw InterruptedException because wait() and Thread.sleep() can be interrupted by other threads. Handling this exception ensures that if the thread is interrupted while waiting or sleeping, it can react appropriately by restoring the interrupted state or performing necessary cleanup.

Coordinating Multiple Threads with wait() and notify()

Now that we have an understanding of the countdown mechanism, let's see how it is used to coordinate two threads. One thread will wait for the countdown, while the other will perform the countdown and notify the waiting thread.

Java
1public class Main { 2 public static void main(String[] args) throws InterruptedException { 3 Countdown countdown = new Countdown(); 4 5 // Thread that waits for the countdown 6 Thread waiter = new Thread(() -> { 7 try { 8 countdown.waitForCountdown(); 9 } catch (InterruptedException e) { 10 Thread.currentThread().interrupt(); 11 } 12 }); 13 14 // Thread that performs the countdown 15 Thread notifier = new Thread(() -> { 16 try { 17 countdown.startCountdown(); 18 } catch (InterruptedException e) { 19 Thread.currentThread().interrupt(); 20 } 21 }); 22 23 waiter.start(); 24 Thread.sleep(1000); // Ensure the waiting thread starts first 25 notifier.start(); 26 27 waiter.join(); 28 notifier.join(); 29 } 30}

In this example:

  • Two threads are created: one that waits (waiter) and one that performs the countdown (notifier).
  • A short delay (Thread.sleep(1000)) ensures that the waiter starts before the countdown begins, ensuring correct order of execution.
  • The join() method is used to ensure both threads finish before the program terminates, ensuring a clean and controlled shutdown.

Running the above code results in the following output:

1Waiting for countdown... 2Countdown started... 3Countdown finished! Notified waiting thread. 4Countdown complete! Proceeding...

Here’s what happens:

  1. Thread waiter starts first and immediately calls the waitForCountdown() method. Since isReady is false, the wait() method is called, causing the thread to wait.
  2. After a brief delay of 1 second, thread notifier starts and calls the startCountdown() method. It simulates a countdown by sleeping for 2 seconds, then sets isReady to true and calls notify() to wake the waiting thread.
  3. Once the waiting thread is notified, it resumes, completing its task and printing "Countdown complete! Proceeding...".

This setup demonstrates the power of thread coordination using wait() and notify(). The waiting thread only proceeds when it is signaled, ensuring proper synchronization between the two threads.

Why It Matters

Understanding and using wait() and notify() is essential for managing inter-thread communication in multithreaded programs. These methods help you control the execution flow of threads, ensuring that threads wait for necessary conditions before proceeding.

Here’s why these concepts are important:

  • Efficient Communication: wait() and notify() allow threads to communicate efficiently, reducing CPU usage by avoiding busy-waiting or continuous polling.
  • Prevention of Resource Contention: By coordinating thread actions, we prevent race conditions and resource conflicts, ensuring that shared resources are used efficiently and safely.
  • Improved System Performance: Proper synchronization improves the overall performance of your application, reducing the chances of deadlock, race conditions, or bottlenecks.

Mastering thread coordination techniques like these enables you to develop robust, high-performance, and scalable multithreaded applications.

Now that you understand how to use wait() and notify() for thread coordination, let’s move to the practice section and apply these concepts!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.