This article has participated in the call for good writing activities, click to view: back end, big front end double track submission, 20,000 yuan prize pool waiting for you to challenge!
Multithreaded concurrency is a very important content in Java language, but also a difficult point in Java foundation. It is important because multithreading is frequently used in daily development, and it is difficult because there are so many knowledge points involved in multithreading concurrency that it is not easy to fully master Java concurrency knowledge. For this reason, Java concurrency is one of the most frequently covered topics in Java interviews. This series of articles will take a systematic look at Java concurrency in terms of the Java memory model, volatile keywords, synchronized keywords, ReetrantLock, Atomic concurrency classes, and thread pools. In this series of articles, you will learn more about the use of volatile, the implementation of synchronized, AQS and CLH queue locking, and clearly understand spin locking, bias locking, optimistic locking, pessimistic locking… And so on a dizzying array of concurrent knowledge.
Multi-threaded concurrency series
This time, understand the Java memory model and the volatile keyword once and for all
This time, thoroughly understand the Synchronized keyword in Java
This time, thoroughly understand the Java ReentranLock implementation principle
This time, understand Java thoroughly and send out the Atomic Atomic classes in the package
Understanding the wait and wake up mechanism of Java threads
Understanding the wait and wake up mechanism of Java threads (Part 2)
Java Concurrency series finale: Get to the bottom of how Java thread pools work
The principle of ThreadLocal is simple
This article, the fifth in the Java concurrency series, takes an in-depth look at Java’s wake up and wait mechanism.
About the thread of waiting and wake up must be no stranger to everyone, after all, in the beginning of learning Java foundation is the focus of learning content. In the previous two articles analyzing synchronized and ReentranLock, we skipped the contents related to thread wait and wake up, mainly because it is not easy to deeply understand thread wait and wake up mechanism, so we will write a separate article to analyze this knowledge point. In this article, we will further divide the waiting and awakening of the next thread from two aspects of synchronized and ReentranLock.
Before I start, I would like to recommend the GitHub repository AndroidNote, which is my study notes and the source of the first draft of my article. This repository contains a lot of Java and Android advancements. Is a systematic and comprehensive Android knowledge base. It is also a valuable interview guide for students preparing for interviews. Welcome to the GitHub warehouse homepage.
A view of thread wait and wake up from synchronized lock
When you first learned Java, you must have used synchronized to implement the code of “producer-consumer” model, which used several Object methods such as wait(), notify() and notifyAll(). I don’t know if you were confused at that time. Why are methods of thread waiting related to wakeup defined in the Object class?
What? Have you forgotten what the producer-consumer model is? Well, let’s start by reviewing the producer-consumer model.
1. The producer-consumer model
The producer-consumer model is a typical example of thread cooperative communication. There are two types of roles in this model, namely producer threads and consumer threads. The producer thread is responsible for submitting user requests, and the consumer thread is responsible for processing requests submitted by the producer. In many cases, producers and consumers do not reach a certain balance, that is, sometimes producers produce too fast for consumption; Sometimes consumers are too busy for producers to produce. In this case, a producer and consumer shared memory cache is needed to balance the collaboration between the two. Producers and consumers communicate through a shared memory cache to balance and decouple producer and consumer threads. As shown below:
When there are no items in the queue container, consumers need to be in a waiting state, and when the container is full, producers need to be in a waiting state. And every time a consumer consumes a good, it notifies the waiting producer that it is ready to produce. When production produces a good, it also notifies the waiting consumer that it is ready to consume.
2. Use synchronized to realize the producer-consumer model
Knowing the producer-consumer model, we try to implement an example of the producer-consumer model using the synchronized keyword combined with wait() and notifyAll() methods.
Let’s choose a more classic example of bread production. First, we need a bread container class. The container class has two operations: put bread and take bread.
public class BreadContainer {
LinkedList<Bread> list = new LinkedList<>();
// Container capacity
private final static int CAPACITY = 10;
/** * put the bread */
public synchronized void put(Bread bread) {
while (list.size() == CAPACITY) {
try {
If the container is full, the producer thread is blocked
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(bread);
// Notify the consumer thread when the bread is successfully produced
notifyAll();
System.out.println(Thread.currentThread().getName() + " product a bread" + bread.toString() + " size = " + list.size());
}
/** * take out the bread */
public synchronized void take(a) {
while (list.isEmpty()) {
try {
If the container is empty, the consumer thread is blocked
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Bread bread = list.removeFirst();
// After consumption, producers are notified to produce bread
notifyAll();
System.out.println("Consumer " + Thread.currentThread().getName() + " consume a bread" + bread.toString() + " size = "+ list.size()); }}Copy the code
The put method in the above code puts the produced bread into the container. If the container is full, the producer thread needs to be blocked to stop production, and when the producer successfully puts the bread into the container, it needs to try to wake up the waiting consumer thread to consume it.
When the container is empty, it blocks the consumer thread and makes it wait. If the bread is consumed successfully, it notifies the producer to start production.
Note that both methods use the synchronized keyword, and that the synchronized object is the instance of the synchronized method. The wait() and notifyAll() methods are also BreadContainer objects. Remember this passage, put a Flag here, we’ll analyze it later.
Then the producer and consumer implementation is relatively simple, with the following code:
/ / producer
public class Producer implements Runnable {
private final BreadContainer container;
public Producer(BreadContainer container) {
this.container = container;
}
@Override
public void run(a) {
// Producers produce bread
container.put(newBread()); }}/ / consumer
public class Consumer implements Runnable {
private final BreadContainer container;
public Consumer(BreadContainer container) {
this.container = container;
}
@Override
public void run(a) {
// Consumers consume breadcontainer.take(); }}Copy the code
Next, in the test code, open multiple producer threads and multiple consumer threads simultaneously
public static void main(String[] args) {
BreadContainer container = new BreadContainer();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
new Thread(new Producer(container)).start();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
new Thread(new Consumer(container)).start();
}
}).start();
}
Copy the code
Run the main method at this point, and the producer and consumer threads work well together.
Notice that the main method instantiates a BreadContainer object. The synchronized lock object mentioned above in Flag is the container. The wait and notifyAll methods are also the methods of the Container instance. What do the wait and notify methods of Containers do to cause threads to block and wake up? Where does the blocked thread go? Why call the wait and notifyAll methods in the Container object? Is it possible to call wait and notifyAll of other objects instead?
Implementation principles of Wait () and Notify
Using the synchronized keyword, a synchronized object is associated with a monitor object. When a thread acquires a synchronized lock, The count in the Monitor object is incremented by one, and the thread ID is stored in the _ower of the monitor. At this point, if another thread attempts to hold the lock, it will be placed in the _WaitSet queue and wait.
Remember the Flag we set in the last video? Synchronized locks container objects, and wait and notify are also methods of containers, which gives some idea of the problems we left behind in the previous section. Is a thread put into a wait queue when a wait method is called and woken up when a notify or notifyAll method is invoked? To answer this question, we need to look at what wait and notify/notifyAll do.
Let’s take a look at the implementation of wait, notify, and notifyAll methods in Object
public class Object {
public final native void notify(a);
public final native void notifyAll(a);
public final void wait(a) throws InterruptedException {
wait(0L);
}
public final native void wait(long timeoutMillis) throws InterruptedException;
}
Copy the code
Unfortunately, these methods are native, which means they are implemented in the VIRTUAL machine using C/C++. In this case, we might as well scratch the virtual machine code to find out, after all, there is no proof.
1. Implementation of WAIT on VMS
Following on from the producer-consumer model in the previous section, when a producer thread puts bread into a container and finds that the container is full, it calls wait. At this point, the thread releases the lock and blocks.
CPP objectMonitor :: WAIT (jLong millis, bool interruptible, TRAPS) function,ObjectMonitor:: Wait core code:
void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
/ /... Omit other code
// The current thread
Thread * const Self = THREAD ;
// Encapsulate the thread as ObjectWaiter
ObjectWaiter node(Self);
// Mark the state as Wait
node.TState = ObjectWaiter::TS_WAIT ;
Self->_ParkEvent->reset() ;
Thread::SpinAcquire (&_WaitSetLock, "WaitSet - add");// Add the thread to the wait queue
AddWaiter (&node) ;
Thread::SpinRelease (&_WaitSetLock) ;
// ...
// The thread exits monitor and releases the lock
exit (true, Self) ;
}
Copy the code
As you can see, after the wait function is called, the thread is wrapped into an ObjectWaiter object, and the addWait function is called to add the thread to the wait queue. Now look at the code for the addWait function:
inline void ObjectMonitor::AddWaiter(ObjectWaiter* node) {
if (_WaitSet == NULL) {
// Initialize _WaitSet, that is, only one node element
_WaitSet = node;
// _WaitSet is a two-way circular list
node->_prev = node;
node->_next = node;
} else {
/ / head node
ObjectWaiter* head = _WaitSet ;
// The prev of the head of the loop chain is the tail
ObjectWaiter* tail = head->_prev;
assert(tail->_next == head, "invariant check");
// Insert node into the tail of _WaitSettail->_next = node; head->_prev = node; node->_next = head; node->_prev = tail; }}Copy the code
The AddWaiter function is a relatively simple implementation that initializes a _WaitSet list and inserts node to the end of the _WaitSet queue, which is a circular, bidirectional list.
After the thread completes the insertion queue operation, it continues to call the exit function to release the monitor lock and suspend itself.
2. Implement notify on the VM
NotifyAll is called to wake up the consumer thread after the producer has produced the bread. Here we take notify as an example to see the implementation of the notify function in objectMonitor. CPP
void ObjectMonitor::notify(TRAPS) {
int Policy = Knob_MoveNotifyee ;
// Fetch the ObjectWaiter queue from _WaitSet
ObjectWaiter * iterator = DequeueWaiter() ;
if(iterator ! =NULL) {
ObjectWaiter * List = _EntryList ;
// Execute different logic according to the Policy. The default value of Policy is 2
if (Policy == 0) { // prepend to EntryList
// ...
} else if (Policy == 1) { // append to EntryList
// ...
} else if (Policy == 2) { // prepend to cxq
// prepend to cxq
if (List == NULL) {
iterator->_next = iterator->_prev = NULL ;
_EntryList = iterator ;
} else {
iterator->TState = ObjectWaiter::TS_CXQ ;
for (;;) {
ObjectWaiter * Front = _cxq ;
iterator->_next = Front ;
if (Atomic::cmpxchg_ptr (iterator, &_cxq, Front) == Front) {
break; }}}}else if (Policy == 3) { // append to cxq
// ...}}// ...
}
Copy the code
In the notify function, Knob_MoveNotifyee is assigned to Policy, and Knob_MoveNotifyee is a constant with a value of 2, so we’ll only focus on the case where Policy is 2. DequeueWaiter() is a function with the following code:
inline ObjectWaiter* ObjectMonitor::DequeueWaiter(a) {
// dequeue the very first waiter
ObjectWaiter* waiter = _WaitSet;
if (waiter) {
DequeueSpecificWaiter(waiter);
}
return waiter;
}
Copy the code
This function takes the _WaitSet set and assigns it to the iterator. If the iterator is not NULL, there is a blocked thread.
We then assign _EntryList to List, and if List is NULL, there is no thread waiting to acquire the lock. The iterators in the block are then formed into a two-way loop list and _EntryList is pointed to the iterator. Indicates that the blocking thread is able to acquire the lock, but the thread has not yet been awakened.
If _EntryList is not NULL, interator is added to the _CXQ queue through CAS and the _CXQ pointer points to Interator.
You can see that the notify function only queues the thread and does not actually wake it up. The actual operation to wake up the thread is in exist, which is executed after the VIRTUAL machine reads the Monitorexist instructions. The simplified code of Exist is as follows:
void ObjectMonitor::exit(bool not_suspended, TRAPS) {
// ...
for (;;) {
ObjectWaiter * w = NULL ;
// QMode defaults to 0
int QMode = Knob_QMode ;
if (QMode == 2&& _cxq ! =NULL) {
/ /... This takes the head node from the _CXq queue and wakes it up, regardless of omission.
return ;
}
// ...
w = _EntryList ;
// Check whether _EntryList is empty
if(w ! =NULL) {
assert (w->TState == ObjectWaiter::TS_ENTER, "invariant");// _EntryList is not empty, and directly wakes up the head of the _EntryList queue
ExitEpilog (Self, w) ;
return ;
}
// _EntryList is empty, point w to _CXq
w = _cxq ;
if (w == NULL) continue ;
// ...
if (QMode == 1) {
// ...
} else {
// The _CXq queue is not empty
// QMode == 0 or QMode == 2
// If the _CXq queue is empty, point _EntryList to _CXq queue
_EntryList = w ;
ObjectWaiter * q = NULL ;
ObjectWaiter * p ;
// Create a bidirectional ring list
for(p = w ; p ! =NULL ; p = p->_next) {
guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant"); p->TState = ObjectWaiter::TS_ENTER ; p->_prev = q ; q = p ; }}if(_succ ! =NULL) continue;
w = _EntryList ;
if(w ! =NULL) {
guarantee (w->TState == ObjectWaiter::TS_ENTER, "invariant");// Wake up the _EntryList header
ExitEpilog (Self, w) ;
return; }}// Release lock and wake up thread
void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) {
assert (_owner == Self, "invariant");// Exit protocol:
// 1. ST _succ = wakee
// 2. membar #loadstore|#storestore;
// 2. ST _owner = NULL
// 3. unpark(wakee)
_succ = Knob_SuccEnabled ? Wakee->_thread : NULL ;
ParkEvent * Trigger = Wakee->_event ;
Wakee = NULL ;
/ / releases the lock
OrderAccess::release_store_ptr (&_owner, NULL); OrderAccess::fence() ; // ST _owner vs LD in unpark()
// Wake up the thread
Trigger->unpark() ;
/ /...
}
Copy the code
The code of exist is rather complicated, which has been simplified here. Since the default value of QMode is 0, only this case will be discussed.
1) First, if _EntryList is not NULL, ExitEpilog is called directly to fetch the header from _EntryList and wake up the thread;
2) If _EntryList is NULL, but _CXQ queue is not NULL, then fetch all the elements in _CXQ queue and construct a two-way ring-linked list, assign _EntryList to it, and set _CXQ to NULL;
3) Finally, the ExitEpilog function is called to release the lock and wake up the _EntryList header.
Third, summary
This article starts from a simple “producer-consumer” model, recognizes wait and notify/notifyAll methods in Object, and deeply analyzes the implementation of these two methods in virtual machine. The synchronized keyword in Java code by the compiler compiled into bytecode monitorenter/monitorexist instruction, when the virtual machine to execute commands to the relevant will call virtual machine related to the underlying function, take the lock and the operation of the lock is released. Because the lock Object is associated with the Monitor Object, wait and notify/notifyAll can be called to block and wake up threads on this Object instance. The wait function encapsulates the thread as WaitObject and puts it into the wait queue, while the notify/notifyAll function takes out the thread object and puts it into the EntryList queue and wakes up after releasing the lock.
The wait and wake mechanism of synchronized locks obviously has one drawback. Using the producer-consumer model as an example, because both producer and consumer threads are added to the same WaitSet queue, notifyAll cannot precisely control which type of thread is awakened. And in this time, thoroughly understand the Java ReentranLock implementation principle in this article we know ReentranLock, ReentranLock and synchronized have similar wait and wake up mechanism, and can accurately control the specified thread wake up. So how does ReentranLock work? We’ll talk about that next time.
Reference & Recommended reading
Blocking (3) : CXQ, EntryList, and WaitSet