What is the internal principle of AQS?

AQS internal principle analysis

To analyze the internal principles of AQS, you need to grasp the key points, because the internals of AQS are relatively complex, the code is very long and very difficult to read, if you plunge into the source code as soon as you come up, it is difficult to fully grasp it. Therefore, in the second, the three core parts of AQS were extracted as the key points, and these three parts were used as entry points to open the door of AQS.

What are the three major parts? The three core parts of AQS are important methods such as state, queue, and acquisition/release that are expected to be implemented by collaboration tools . Starting from these three parts, analyze separately.

state state

The first thing to explain is the state state. If our AQS wants to manage or serve as a basic framework for collaboration tools, then it must manage some state, and this state is represented by the state variable inside AQS . It is defined as follows:

/**
 * The synchronization state.
 */
private volatile int state;

The meaning of state is not static, it will express different meanings according to the different roles of specific implementation classes . Here are a few examples.

For example, in a semaphore, state represents the number of remaining licenses . If we initially set the state to 10, it means that there are 10 licenses initially, and then when a thread takes a license, the state will become 9, so the state of the semaphore is equivalent to an internal counter.

For another example, in the CountDownLatch tool class, state represents the number that needs to be "counted down" . At the beginning, we assumed that it was set to 5. When the CountDown method is called each time, the state will be reduced by 1, and when it is reduced to 0, it means that the latch is released.

Let's take a look at the meaning of state in ReentrantLock. In ReentrantLock, it represents the lock occupancy . The initial value is 0, which means that no thread holds the lock; if the state becomes 1, it means that the lock has been held by a thread.

Then why does it become 2, 3, 4? Why is it added up? Because ReentrantLock is reentrant, the same thread can own the lock again is called reentrant . If the lock is acquired multiple times by the same thread, the state will gradually increase, and the value of state represents the number of reentrants. When released, it is gradually decreased. For example, when it is 4 at the beginning, it becomes 3 after release, and becomes 2 after release. The reduction operation performed in this way, even if it is reduced to 2 or 1, does not represent the lock There is no thread holding it, only when it is reduced to 0, it is restored to the original state at this time, which means that no thread holds the lock now. Therefore, state equal to 0 means that the lock is not occupied by any thread, which means that the lock is currently in a released state, and other threads can try to acquire it at this time.

This is a concrete manifestation of the different meanings of state in different classes. We have cited three examples. If a new tool wants to use AQS in the future, it must also use state to represent the business logic and state it needs for this class.

Let's look at the issue of state modification again. Because state is shared by multiple threads and modified concurrently, all methods to modify state must ensure that state is thread-safe. But the state itself is only modified by volatile, and volatile itself is not enough to ensure thread safety, so let's take a look at what kind of design AQS uses to ensure concurrency safety when modifying the state.

We cite two methods related to state, namely compareAndSetState and setState. Their implementation has been completed by AQS. That is to say, we can directly call these two methods to make thread-safe modifications to state. Let's take a look at how the source code of these two methods is implemented.

  • Let's first look at the compareAndSetState method, which is a CAS operation that we are very familiar with. The code for this method is as follows:
protected final boolean compareAndSetState(int expect, int update) {
    
    
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

There is only one line of code in the method, namely return unsafe.compareAndSwapInt(this, stateOffset, expect, update). This method is already very familiar to us. It uses the CAS operation in Unsafe, and uses the atomicity of CPU instructions to ensure the atomicity of this operation. It is consistent with the principle of the atomic class to ensure thread safety introduced before.

  • Next, take a look at the source code of the setState method, as shown below:
protected final void setState(int newState) {
    
    
    state = newState;
}

As we can see, it is very straightforward to modify the state value, and directly assign state = newState, so that the value is directly assigned. You may be confused. There is no concurrent safety processing, no locks and no CAS. How can you ensure thread safety?

Here we are going to talk about the role of volatile. When I mentioned the volatile keyword earlier, I knew that it is applicable to two scenarios. One of the scenarios is that when a basic type of variable is directly assigned, if volatile is added You can ensure its thread safety. Note that this is a very typical usage scenario for volatile.

/**
 * The synchronization state.
 */
private volatile int state;

It can be seen that state is of type int, which is a basic type, and the setState method here is directly assigned to state. It does not involve reading the previous value, nor does it involve modifying the original value, so we Only using volatile can guarantee concurrency safety in this case, which is why the setState method is thread-safe.

The following is a summary of state. There is an attribute such as state in AQS, which is modified by volatile and will be modified concurrently. It represents a certain state of the current tool class and has different meanings in different classes.

FIFO queue

Let's take a look at the second core part of AQS, the FIFO queue, that is, the first-in first-out queue. The main function of this queue is to store waiting threads. Assuming that many threads want to grab the lock at the same time, then most of the threads can't grab the lock. How to deal with the threads that can't grab the lock? You need to have a queue to store and manage them. Therefore, a major function of AQS is to act as a " queue manager" for threads .

When multiple threads compete for the same lock, it is necessary to use a queuing mechanism to string together those threads that failed to obtain the lock; and after the current thread releases the lock, the manager will select a suitable thread. Try to grab the lock that was just released. So AQS has been maintaining this queue, and put all the waiting threads in the queue.

This queue is in the form of a doubly linked list, and its data structure seems simple, but it is very complicated to maintain a thread-safe two-way queue, because many multi-thread concurrency issues must be considered. Let's take a look at an illustration of this queue given by AQS author Doug Lea:
Insert picture description here
(This picture is quoted from the picture in the English document )

In the queue, head and tail are used to represent the head node and tail node, respectively, both of which point to an empty node when they are initialized. The head node can be understood as "the thread currently holding the lock", and the threads after the head node are blocked, and they will wait to be awakened. AQS is also responsible for waking up.

Acquire/release method

Let's take a look at the third core part of AQS, the acquisition/release method. In AQS, in addition to the state and queue just mentioned, there is another part that is very important, that is , the important methods of acquiring and releasing . These methods are the concrete manifestation of the logic of the collaboration tool class. Each collaboration tool class needs to go by itself. Implementation , so in different tool classes, their implementation and meaning are different.

How to Obtain

Let's first look at the acquisition method. The fetching operation usually depends on the value of the state variable. Depending on the state value, the collaboration tool class will also have different logic, and it will often block when fetching. Let's look at a few specific examples below.

For example, the lock method in ReentrantLock is one of the "acquisition methods". During execution, if it is found that the state is not equal to 0 and the current thread is not the thread holding the lock, it means that the lock has been held by other threads. At this time, of course, the lock cannot be acquired, so the thread is allowed to enter the blocking state.

For another example, the acquire method in Semaphore is one of the "acquisition methods", which is used to obtain a license. At this time, whether or not the license can be obtained also depends on the value of state. If the state value is a positive number, then it means that there are remaining licenses, and if the number is sufficient, it can be successfully obtained; but if the state is 0, it means that there are no more free licenses, and this thread cannot obtain it. The license will enter the blocking state, so here is also related to the value of state.

For another example, the CountDownLatch acquisition method is the await method (including overloaded methods), which is used to "wait until the end of the countdown". When executing await, the value of state will be judged. If state is not equal to 0, the thread will fall into a blocked state until other threads execute the countdown method to reduce state to 0, which means that the latch is now released, so the thread that was blocked before Will be awakened.

Let me summarize that the "acquisition method" has different meanings in different classes, but it is often related to the state value , and often causes the thread to enter the blocking state, which also proves the important position of the state state in the AQS class.

Release method

The release method is the opposite of the acquisition method, and is usually used in conjunction with the acquisition method just now. The acquisition method we just talked about may block the thread. For example, if the lock is not acquired, the thread will enter the blocking state, but the release method usually does not block the thread .

For example, in the Semaphore semaphore, release is the release method (including overloaded methods), and the function of the release() method is to release a license, which will increase the state by 1. In CountDownLatch, release is the countDown method, and the function is the countdown A number, let state minus 1. Therefore, it can also be seen that in different implementation classes, their operations on state are completely different, and each collaboration class needs to be implemented according to its own logic.


Further reading Let's do some further reading to analyze the core structure of AQS. It is of great benefit to understanding the internal structure of AQS, but it is not enough to include the full picture of AQS. If you are interested in further understanding AQS, you can choose to learn related expansion resources:

  • The first resource is a paper written by Doug Lea, the author of AQS. This paper is naturally a very valuable learning material. Please click here to view ;
  • The second is an article from the Javadoop blog on AQS source code analysis. If you are interested, you can also read it. Please click here to view .
to sum up

The three most important parts of AQS. The first is state, which is a value, which means different meanings in different classes, and often represents a state; the second is a queue, which is used to store threads; the third is "acquisition/release" The related methods of AQS need to be implemented according to their own logic using the tool classes of AQS.

Guess you like

Origin blog.csdn.net/Rinvay_Cui/article/details/114017718