Java Concurrency and Lock Design Implementation Details (5) - Implementation of AQS (AbstractQueuedSynchronizer)

In this article, we will look at a very, very important thing, which is AbstractQueuedSynchronizer, or AQS for short, also known as queue synchronizer.

Why is it important? Because it is the core of the Java concurrent package Java.util.concurrent, when it comes to Java concurrency, if you don't know AQS, it may be a bit too much to say that you understand concurrency ^_^

Who wrote the Java.util.concurrent concurrent package? It's Doug Lea, the great god below. Let's take a look first, although I will never see it in my life haha


Looking kind, he proposed the JSR166 specification, the core of which is the AbstractQueuedSynchronizer Synchronizer Framework (AQS), which provides a general mechanism for constructing synchronizers in Java.

In JDK1.7, we can view the source code and roughly see what classes are there, as follows:


Among them, AbstractOwnableSynchronizer is its parent class, and AbstractQueuedLongSynchronizer is an upgraded 64-bit implementation of its 32-bit state, which is suitable for multi-level barriers (CyclicBarrier).

So what is AQS and what is it used for? I intercepted a basic description of this class in the source code, as follows:

/**
 * Provides a framework for implementing blocking locks and related
 * synchronizers (semaphores, events, etc) that rely on
 * first-in-first-out (FIFO) wait queues.  This class is designed to
 * be a useful basis for most kinds of synchronizers that rely on a
 * single atomic <tt>int</tt> value to represent state. Subclasses
 * must define the protected methods that change this state, and which
 * define what that state means in terms of this object being acquired
 * or released.  Given these, the other methods in this class carry
 * out all queuing and blocking mechanics. Subclasses can maintain
 * other state fields, but only the atomically updated <tt>int</tt>
 * value manipulated using methods {@link #getState}, {@link
 * #setState} and {@link #compareAndSetState} is tracked with respect
 * to synchronization.
 *

The translation is as follows: This class provides a framework for implementing blocking locks and related synchronizers (such as semaphores, events, etc.) that rely on FIFO wait queues. This class is designed to be a cornerstone of many other synchronizers that use an atomic int to represent the synchronization state. Subclasses that inherit this class must redefine some protected methods that change the synchronization state, including the acquisition, acquisition and release of the synchronization state. For this reason, other methods in AQS implement all queuing and blocking mechanisms. Of course, subclasses can also maintain their own state fields, but require atomic update state values. (Personal English level is limited, if there is something wrong, please forgive me!)

Based on the above description, we can know that the AQS framework is designed based on the template method, providing basic access restriction control, and the subclass defines the state change rules by itself, so as to realize different synchronizers.


In the AQS framework, two resource sharing modes are defined.

Exclusive: Exclusive mode, only one thread can execute at the same time, such as ReentrantLock

Share: Shared mode, multiple threads can execute at the same time, such as Semaphore/CountDownLatch.

Generally speaking, custom synchronizers are either exclusive methods or shared methods, and they only need to implement one of tryAcquire-tryRelease and tryAcquireShared-tryReleaseShared. But AQS also supports custom synchronizers to implement both exclusive and shared methods, such as ReentrantReadWriteLock.

When implementing a custom synchronizer, you only need to implement the acquisition and release methods of the shared resource state. As for the maintenance of specific threads waiting for the queue (such as the failure to acquire resources to enter the queue/wake up and dequeue, etc.), AQS has been implemented at the top level. When implementing a custom synchronizer, it mainly implements the following methods:

  • isHeldExclusively(): Whether the thread is exclusively using resources. You only need to implement it if you use a condition.
  • tryAcquire(int): Exclusive mode. Attempt to get the resource, return true on success, false on failure.
  • tryRelease(int): Exclusive mode. Attempt to release the resource, return true on success, false on failure.
  • tryAcquireShared(int): Shared method. Try to get the resource. A negative number means failure; 0 means success, but no remaining available resources; a positive number means success, and there are remaining resources.
  • tryReleaseShared(int): Shared method. Attempt to release the resource, return true on success, false on failure. 
In order to achieve the above operations, AQS requires the cooperation of the following three basic components :
  • Atomic management of synchronization state;
  • Thread blocking and unblocking;
  • management of queues;
(1) Atomic management of synchronization state

In AQS, the synchronization state is identified by modifying an int variable with a volatile keyword (as to why a 32-bit Int variable is used here instead of a 64-bit Long variable, it will not be explained here, if you are interested, you can consider it yourself Next), the atomicity and visibility of synchronous state updates are guaranteed through CAS and volatile.

For the description of the volatile keyword, please refer to my other blog < Detailed Implementation of Java Concurrency and Lock Design (9) - The underlying principle of keyword volatile >.


As for CAS, we have already mentioned in this article < Java Concurrency and Lock Design Implementation Details (6) - Let's Talk About Unsafe >, in the JAVA concurrency framework, the core of the concurrent package is AQS, and the core of AQS is UnSafe Class, the UnSafe class provides the native method CAS for synchronization, which is used for atomic operations of various types of variables.


(2) Blocking and unblocking of threads

In AQS, thread blocking and wake-up are implemented through LockSupport.park() and LockSupport.unpark() (the bottom layer calls Unsafe's native park and unpark implementation), and supports timeout. As follows:


(3) Queue management

When a thread tries to acquire a synchronization lock, if it can be directly acquired successfully, it will return directly, otherwise the current thread will be added to the waiting queue and spin in the queue until it acquires the synchronization lock or is interrupted to exit. The process does not respond to interrupts.


Let's combine the source code to see how to obtain an exclusive lock.

Get the entry for an exclusive lock:

From the logic of the code, it can be understood as: (1) first try to acquire an exclusive lock; (2) if no exclusive lock is acquired, add the current thread to the waiting queue and spin to wait to acquire an exclusive lock; (3) If a thread interrupt is encountered during the spinning process (the corresponding interrupt is not performed during the spinning process), the interrupt processing is also performed;

Let's take a look at how each step is done in detail.

The first is tryAcquire(arg), which is not implemented in AQS. The purpose is to allow the inherited class to control whether it can acquire the lock. The inherited class needs to implement it by itself. Next is addWaiter(Node.EXCLUSIVE), which implements adding the current thread to the waiting queue.


First create a Node node above, and then judge whether there is a tail node. If the node is not empty, it will try to quickly insert a node from the tail to the tail through compareAndSetTail. If the attempt to insert fails, then follow the normal process to insert a node into the queue, that is, call enq(node) again to insert the node.


We see here that a for(;;) infinite loop is used for spin, and only the node is successfully inserted will it exit from the loop. Every time it tries to use the CAS operation to update the tail node, if the update is successful, it is considered that the insertion into the queue is successful, otherwise it is considered that it fails and the next attempt will be made.

After encapsulating the thread that has not acquired the synchronization lock into the Node node and adding it to the waiting queue, what will be done later? Returning to the acquire method above, you can know that an acquireQueued method is performed immediately. What does this method do? Take a look at the source code:


There are two local variables, failed and interrupted. Failed is used to record whether the synchronization lock is successfully acquired, and interrupted is used to record whether there is an interruption in the park process to record the interruption status.

On the whole, it is a for(;;) infinite loop. There is only one case in this loop that can jump out of the loop, that is, when there are no other Node nodes waiting for the synchronization lock in front of the current node in the waiting queue, and the current node successfully obtains the Synchronization lock will jump out of the infinite loop. If this is not the case above, it will keep trying to wait.

This code is to identify the Node node that is not waiting for the synchronization lock in front of the current node, and returns successfully after successfully acquiring the synchronization lock through tryAcquire(arg).

If not, the spin continues to perform the following processing:

Let's see what these two methods do. One is shouldParkAfterFailedAcquire(p, node), and the other is parkAndCheckInterrupt().

The former should refer to whether the park is required after the failure to acquire the synchronization lock, and the latter should be to perform the park operation and check whether an interruption is encountered.

First look at the former source code:


As can be seen from the first sentence of the description of this method, this method is used to check and update the node state value after the failure to acquire the synchronization lock. So what are the state values ​​of the nodes? I found a piece of Node node state definition in the source code:

waitstatus is divided into 4 states: CANCELED (1), SIGNAL (-1), CONDITION (-2), PROPAGATE (-3).

Go back to the logic of shouldParkAfterFailedAcquire(p, node), and perform corresponding processing according to the state value of p in the previous section. If the predecessor node is in the SIGNAL state, then return true, indicating that the current node must be parked. If the predecessor node is canceled, that is, ws>0, then start from the predecessor node to find a node with ws<=0 and return false, indicating that the operation does not require park, and the next spin retry is required. If it is not in these two cases, then you need to update the state of the precursor node to SIGNAL state and return false, indicating that the spin process does not require park.

Next, let's look at another method. In the case of needing park, you need to continue to call the parkAndCheckInterrupt() method:


This method implements the park function with the help of the LockSupport tool class and returns the interrupt status of the thread.

After talking about these two methods, we return to the previous acquireQueued method. At the end of the method, we see that if it fails, the cancelAcquire operation will be performed. What is this operation for? Let's take a look at the source code:


The above code labeled 1 updates the thread and ws state in the current Node node, the code labeled 2 filters the canceled threads before the current node, and the code labeled 3 is used to quickly determine whether the current node is is the tail node, if so, delete the node directly. The code numbered 4 is used to process the current Node node that is not the tail node.

Finally, go back to the acquire(arg) method. If an interrupt is encountered during the acquireQueued() process, the selfInterrupt method will be called, as follows:

This will return the interrupt status of the current thread.

At this point, the process of obtaining an exclusive lock at one time is all finished, and interested friends can look at the source code by themselves. Corresponding to acquiring a synchronization lock, it should be releasing the synchronization lock. Let's take a look at the specific logic of release(arg).


The first is to call tryRelease to try to release the synchronization lock. If it fails, return false directly. If it succeeds, perform other corresponding processing. The tryRelease method, like the tryAcquire method, is implemented by the inherited class itself according to the required characteristics.

We next unparkSuccessor(h) method,


The purpose of this method is to wake up the current node's successor, if one exists. In the above code, find the real successor node after the current node by going backwards from the tail of the waiting queue (remove the intermediate CANCEL state node). After finding this node, use the LockSupport.unpark method to wake up the successor node of the current node.

So far, the basic framework of AQS has been briefly explained, and the acquisition and release of exclusive locks have been explained in detail in combination with the source code. Of course, AQS not only includes these, but also the acquisition and release of shared locks. This is partly because The time and space issues are not explained here. Interested friends can read the source code by themselves, thank you.

Thank you for reading. If you are interested in Java programming, middleware, databases, and various open source frameworks, please pay attention to my blog and Toutiao (Source Code Empire). The blog and Toutiao will regularly provide some related technical articles for later. Let's discuss and learn together, thank you.

If you think the article is helpful to you, please give me a reward. A penny is not too little, and a hundred is not too much. ^_^ Thank you.


Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325915383&siteId=291194637