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 asynchronized
method. -
condition.await()
corresponds toobject.wait()
. -
condition.signalAll()
corresponds toobject.notifyAll()
. -
lock.unlock()
corresponds to exiting asynchronized
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.