To learn Java concurrency programming, we have to learn about the java.util.concurrent package. There are many concurrency tool classes that we often use under this package, such as: ReentrantLock, CountDownLatch, CyclicBarrier, Semaphore, etc. The underlying implementation of these classes all rely on the AbstractQueuedSynchronizer class, which shows the importance of this class. So in the Java concurrency series, I first analyzed the AbstractQueuedSynchronizer class. Since this class is more important and the code is relatively long, in order to analyze it as thoroughly as possible, I decided to use four articles to give a relatively complete introduction to this class. This article is a summary introduction to give readers a preliminary understanding of this category. For the sake of simplicity of narration, some places will use AQS to represent this class in the future.
1. What is the AbstractQueuedSynchronizer class for?
I believe that many readers have used ReentrantLock, but they do not know the existence of AbstractQueuedSynchronizer. In fact, ReentrantLock implements an internal class Sync, which inherits AbstractQueuedSynchronizer. All lock mechanism implementations rely on Sync internal classes. It can also be said that the implementation of ReentrantLock depends on AbstractQueuedSynchronizer class. Similarly, CountDownLatch, CyclicBarrier, and Semaphore classes also use the same method to implement their own control of locks. It can be seen that AbstractQueuedSynchronizer is the cornerstone of these classes. So what exactly is implemented inside AQS so that all these classes depend on it? It can be said that AQS provides infrastructure for these classes, that is, it provides a password lock. After these classes have a password lock, they can set the password of the password lock by themselves. In addition, AQS also provides a queue area and a thread instructor. We know that threads are like a primitive barbarian. They don’t know how to be polite. They will only rush around, so you have to teach it step by step, tell it when it needs to queue, where to queue, what to do before queueing, and what to do after queueing. All these educational work are completed by AQS for you. The threads educated from it have become very civilized and polite, and are no longer primitive barbarians. So in the future, we only need to deal with these civilized threads. Never have too much contact with the original threads!
2. Why does AbstractQueuedSynchronizer provide a password lock?
//The head node of the synchronization queue private transient volatile Node head; //The tail node of the synchronization queue private transient volatile Node tail;//The private volatile int state;//Get synchronization state protected final int getState() { return state;}//Set synchronization state protected final void setState(int newState) { state = newState;}//Set synchronization state protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update);}The above code lists all member variables of AQS. You can see that there are only three member variables of AQS, namely the synchronization queue head node reference, the synchronization queue tail node reference, and the synchronization state. Note that all three member variables are modified with the volatile keyword, which ensures that multiple threads modify it is memory-visible. The core of the entire class is this synchronization state. You can see that the synchronization state is actually an int-type variable. You can regard this synchronization state as a password lock, and it is also a password lock locked from the room. The specific value of the state is equivalent to the password controlling the opening and closing of the password lock. Of course, the password of this lock is determined by each subclass. For example, in ReentrantLock, state equals 0 means that the lock is open, state greater than 0 means that the lock is locked, and in Semaphore, state greater than 0 means that the lock is open, and state equals 0 means that the lock is locked.
3. How is the queue area of AbstractQueuedSynchronizer implemented?
There are actually two queueing areas inside AbstractQueuedSynchronizer, one is a synchronous queue and the other is a conditional queue. As can be seen from the above figure, there is only one synchronization queue, while there can be multiple condition queues. The nodes of the synchronous queue hold references to the front and back nodes respectively, while the nodes of the conditional queue have only one reference to the successor node. In the figure, T represents a thread. Each node contains a thread. After the thread fails to acquire the lock, it first enters the synchronization queue to queue. If you want to enter the conditional queue, the thread must hold the lock. Next, let's take a look at the structure of each node in the queue.
//The nodes of the synchronous queue are static final class Node { static final Node SHARED = new Node(); //The current thread holds the lock in shared mode static final Node EXCLUSIVE = null; //The current thread holds the lock in exclusive mode static final int CANCELLED = 1; //The current node has cancelled the lock static final int SIGNAL = -1; //The threads of the successor node need to run static final int CONDITION = -2; //The current node is queued in the conditional queue static final int PROPAGATE = -3; //The subsequent node can directly acquire the lock volatile int waitStatus; //Denote the waiting state of the current node volatile Node prev; //Denote the forward node in the synchronization queue volatile Node next; //Denote the successor node in the synchronization queue volatile Thread thread; //The thread held by the current node refers to Node nextWaiter; //Denote the successor node in the conditional queue//Is the current node state in the shared mode final boolean isShared() { return nextWaiter == SHARED; } //Return the forward node of the current node final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) { throw new NullPointerException(); } else { return p; } } //Constructor 1 Node() {} //Constructor 2, this constructor is used by default Node(Thread thread, Node mode) { //Note that the holding mode is assigned to nextWaiter this.nextWaiter = mode; this.thread = thread; } //Constructor 3, only Node(Thread thread, int waitStatus) is used in the condition queue { this.waitStatus = waitStatus; this.thread = thread; }}Node represents a node in the synchronization queue and the conditional queue. It is an inner class of AbstractQueuedSynchronizer. Node has many attributes, such as holding mode, waiting state, pre-sequence and successor in synchronous queues, and successor references in conditional queues, etc. The synchronization queue and condition queue can be regarded as a queue area, each node is regarded as a seat in the queue area, and the thread is regarded as a queue guest. When the guests first come, they will knock on the door to see if the lock is opened. If the lock is not opened, they will go to the queue area to collect a number plate, declare what way they want to hold the lock, and finally queue up at the end of the queue.
4How to understand exclusive mode and sharing mode?
As mentioned earlier, each guest will receive a number plate before queuing and declare that he wants to possess the lock. The way of possessing the lock is divided into exclusive mode and sharing mode. So how do you understand the exclusive mode and sharing mode? I really can't find any good analogy. You can think of a public toilet. People with exclusive mode are more domineering. I either don't go in. When I come in, I don't allow others to go in. I occupy the entire toilet alone. People in the sharing mode are not so particular. When they find that the toilet is already usable, it doesn’t count if it comes in by itself. They also have to enthusiastically ask the people behind whether they mind using it together. If the people behind don’t mind using it together, there is no need to queue up. Everyone will go together. Of course, if the people behind them mind, they have to stay in the queue and continue queuing.
5 How to understand the waiting state of a node?
We also see that each node has a waiting state, which is divided into four states: CANCELLED, SIGNAL, CONDITION, and PROPAGATE. This waiting state can be regarded as a sign hanging next to the seat, identifying the waiting state of the person on the current seat. The status of this brand can not only be modified by yourself, but others can also modify it. For example, when this thread has already planned to give up during the queue, it will set the sign on its seat to CANCELLED, so that others can clear it out of the queue if they see it. Another situation is that when the thread is about to fall asleep in the seat, it is afraid that it will oversleep, so it will change the sign in the front position to SIGNAL, because everyone will return to their seats before leaving the queue to take a look. If it sees that the status on the sign is SIGNAL, it will wake up the next person. Only by ensuring that the brand in the front position is SIGNAL, the current thread will sleep peacefully. The CONDITION status indicates that the thread is queued in the conditional queue. The PROPAGATE status reminds the subsequent threads to acquire the lock directly. This status is only used in the shared mode, and will be discussed later when talking about the shared mode separately.
6. What operations will be performed when a node enters the synchronization queue?
//Node enqueue operation, return to the previous node private Node enq(final Node node) { for (;;) { //Get the reference to the tail node of the synchronization queue Node t = tail; //If the tail node is empty, it means that the synchronization queue has not been initialized if (t == null) { //Initialize the synchronization queue if (compareAndSetHead(new Node())) { tail = head; } } else { //1. Point to the current tail node node.prev = t; //2. Set the current node to the tail node if (compareAndSetTail(t, node)) { //3. Point the successor of the old tail node to the new tail node t.next = node; //The only exit of the for loop returns t; } } }}Note that the enqueue operation uses a dead loop. Only when the node is successfully added to the tail of the synchronization queue will it be returned. The result is the original tail node of the synchronization queue. The following figure shows the entire operation process.
Readers need to pay attention to the order of adding tail nodes, which are divided into three steps: pointing to tail nodes, CAS changes the tail nodes, and pointing the successors of the old tail node to the current node. In a concurrent environment, these three steps may not be guaranteed to be completed. Therefore, in the operation of clearing all canceled nodes in the synchronization queue, in order to find nodes in a non-cancel state, it is not traversed from front to back but from back to front. Also, when each node enters the queue, its waiting state is 0. Only when the thread of the subsequent node needs to be suspended will the waiting state of the previous node be changed to SIGNAL.
Note: All the above analysis is based on JDK1.7, and there will be differences between different versions, readers need to pay attention.
The above is all the content of this article. I hope it will be helpful to everyone's learning and I hope everyone will support Wulin.com more.