55 Relationship Between Condition, Object.wait, and Notify

55 Relationship Between Condition, object #

In this lesson, we mainly introduce the relationship between Condition, Object’s wait(), and notify().

Let’s first talk about the Condition interface and see its purpose, how to use it, and what points to note.

Condition Interface #

Purpose #

Suppose thread 1 needs to wait for certain conditions to be met before it can continue running. This condition may vary depending on the business scenario, such as waiting for a certain time point to be reached or waiting for certain tasks to be completed. In this case, we can execute the await() method of Condition. Once this method is executed, the thread will enter the WAITING state.

Usually, there will be another thread, which we call thread 2, to achieve the corresponding conditions. Until these conditions are met, thread 2 calls the signal() method of Condition to indicate that “these conditions have been met, and the threads waiting for these conditions can now wake up.” At this time, the JVM will find the thread waiting for this Condition and wake it up. Depending on whether the signal method or signalAll method is called, one or all threads will be awakened. Therefore, thread 1 will be awakened at this time, and its thread state will return to Runnable executable state.

Code Example #

Let’s use a code to illustrate this problem, as shown below:

public class ConditionDemo {

    private ReentrantLock lock = new ReentrantLock();

    private Condition condition = lock.newCondition();

    void method1() throws InterruptedException {

        lock.lock();

        try{

            System.out.println(Thread.currentThread().getName()+":条件不满足,开始await");

            condition.await();

            System.out.println(Thread.currentThread().getName()+":条件满足了,开始执行后续的任务");

        }finally {

            lock.unlock();

        }

    }

    void method2() throws InterruptedException {

        lock.lock();

        try{

            System.out.println(Thread.currentThread().getName()+":需要5秒钟的准备时间");

            Thread.sleep(5000);

            System.out.println(Thread.currentThread().getName()+":准备工作完成,唤醒其他的线程");

            condition.signal();

        }finally {

            lock.unlock();

        }

    }

    public static void main(String[] args) throws InterruptedException {

        ConditionDemo conditionDemo = new ConditionDemo();

        new Thread(new Runnable() {

            @Override

            public void run() {

                try {

                    conditionDemo.method2();

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

        }).start();

        conditionDemo.method1();

    }

}

In this code, there are three methods.

  • method1: It represents the content that the main thread is about to execute. First, it acquires the lock, prints “条件不满足,开始await” (Condition is not met, start await), and then calls the condition.await() method. It will continue to execute the statement below until the condition is met. Then it prints “条件满足了,开始执行后续的任务” (Condition is met, start executing subsequent tasks), and finally unlocks in the finally block.
  • method2: It also needs to acquire the lock first, prints “需要5秒钟的准备时间” (Needs 5 seconds of preparation time), and then uses sleep to simulate the preparation time. After the time is up, it prints “准备工作完成,唤醒其他的线程” (Preparation work is completed, wake up other threads), and finally calls the condition.signal() method to wake up the previously waiting threads. It also unlocks in the finally block.
  • main method: Its main function is to execute the above two methods. It first instantiates this class, then uses a sub-thread to call the method2 method of this class, and then uses the main thread to call the method1 method.

The final output of this code program is as follows:

main:条件不满足,开始await (Condition is not met, start await)
Thread-0:需要 5 秒钟的准备时间 (Requires 5 seconds of preparation time)
Thread-0:准备工作完成,唤醒其他的线程 (Preparation work is completed, wake up other threads)
main:条件满足了,开始执行后续的任务 (Condition is met, start executing subsequent tasks)

At the same time, we can see the threads that printed these lines. The first and fourth lines are printed in the main thread, which means they are printed in the main thread. The second and third lines are printed in the sub-thread. This code simulates the scene we described earlier.

Points to Note #

Next, let’s take a look at the important points to note when using Condition.

  • Thread 2 can only acquire the lock after Thread 2 releases the lock and continue to execute

Thread 2 corresponds to the sub-thread in the previous code, and Thread 1 corresponds to the main thread. It is important to note that when the sub-thread calls signal, it does not mean that the main thread can be immediately woken up to execute the code below. Instead, after calling signal, it needs to wait for the sub-thread to completely exit the lock, that is, after executing unlock, the main thread may be able to acquire the lock. And it can only continue to execute subsequent tasks after successfully acquiring the lock. When it is first awakened, the main thread has not acquired the lock, so it cannot continue to execute.

  • Difference between signalAll() and signal()

signalAll() will wake up all waiting threads, while signal() will only wake up one thread.

Implementing a Simple Blocking Queue with Condition and wait/notify() #

In Lesson 05, we discussed how to use Condition and wait/notify() to implement the producer/consumer pattern. The essence of this pattern lies in using Condition and wait/notify() to implement a simple blocking queue. Let’s review these two pieces of code separately.

Implementing a Simple Blocking Queue with Condition #

Here is the code:

public class MyBlockingQueueForCondition {

   private Queue queue;

   private int max = 16;

   private ReentrantLock lock = new ReentrantLock();

   private Condition notEmpty = lock.newCondition();

   private Condition notFull = lock.newCondition();

   public MyBlockingQueueForCondition(int size) {

       this.max = size;

       queue = new LinkedList();

   }

   public void put(Object o) throws InterruptedException {

       lock.lock();

       try {

           while (queue.size() == max) {

               notFull.await();
  }
  
  queue.add(o);
  
  notEmpty.signalAll();
  
} finally {
  
  lock.unlock();
  
}

In the above code, first a queue variable queue with a maximum capacity of 16 is defined. Then a ReentrantLock object lock is defined, and two Condition objects notEmpty and notFull are created based on the lock. notEmpty represents the condition when the queue is not empty, and notFull represents the condition when the queue is not full. Finally, two core methods, put and take, are declared.

Implementing a simple blocking queue using wait/notify #

Let’s see how to use wait/notify to implement a simple blocking queue. The code is as follows:

class MyBlockingQueueForWaitNotify {
  
  private int maxSize;
  
  private LinkedList<Object> storage;
  
  public MyBlockingQueueForWaitNotify(int size) {
    
    this.maxSize = size;
    
    storage = new LinkedList<>();
    
  }
  
  public synchronized void put() throws InterruptedException {
    
    while (storage.size() == maxSize) {
      
      this.wait();
      
    }
    
    storage.add(new Object());
    
    this.notifyAll();
    
  }
  
  public synchronized void take() throws InterruptedException {
    
    while (storage.size() == 0) {
      
      this.wait();
      
    }
    
    System.out.println(storage.remove());
    
    this.notifyAll();
    
  }
  
}

As shown in the code, the most important parts are still the put and take methods. Let’s look at the put method first. The method is protected by synchronized, and it checks whether the list is full in a while loop. If it’s not full, it adds an object to the list and notifies all other threads through notifyAll(). Similarly, the take method is also protected by synchronized and it checks whether the list is empty in a while loop. If it’s not empty, it retrieves an object from the list and notifies all other threads.

In Lecture 5, there is a detailed explanation of these two pieces of code. If you have forgotten, you can review it earlier.

Relationship between Condition and wait/notify #

Comparing the put methods in the two implementation approaches, you will find that they are very similar. Let’s put these two pieces of code side by side on the screen and compare them:

Left:

public void put(Object o) throws InterruptedException {

  lock.lock();

  try {

    while (queue.size() == max) {

      condition1.await();

    }

    queue.add(o);

    condition2.signalAll();

  } finally {

    lock.unlock();

  }

}

Right:

public synchronized void put() throws InterruptedException {

  while (storage.size() == maxSize) {

    this.wait();

  }

  storage.add(new Object());

  this.notifyAll();

}

It can be seen that the left side is the implementation using Condition, and the right side is the implementation using wait/notify:

  • lock.lock() corresponds to entering a synchronized method.

  • condition.await() corresponds to object.wait().

  • condition.signalAll() corresponds to object.notifyAll().

  • lock.unlock() corresponds to exiting a synchronized method.

In fact, if Lock is used to replace synchronized, then Condition is used to replace the corresponding wait/notify/notifyAll of Object, so they are almost the same in terms of usage and nature.

Condition transforms the wait/notify/notifyAll of Object into a corresponding object method, and the implementation effect is basically the same. But it transforms more complex usage into more intuitive and controllable object methods, which is an upgrade.

The await method automatically releases the held lock, just like wait of Object, so there is no need to manually release the lock.

In addition, the lock must be held when calling await, otherwise an exception will be thrown, which is the same as wait of Object.

Summary #

This article first introduced the purpose of the Condition interface and provided the basic usage, then explained several points to note, reviewed the code for implementing a simple blocking queue using Condition and wait/notify, and compared the different implementations of these two methods. Finally, the relationship between them was analyzed.