AQS implementation principle of concurrent programming series
1. What is AQS?
AQS (AbstractQueuedSynchronizer), an abstract queue synchronizer, is the basis of many Lock locks and synchronization components in juc, such as CountDownLatch, ReentrantLock, ReentrantReadWriteLock, Semaphore, etc. it provides interfaces or specific implementations for resource occupation, release, thread waiting, wake-up, etc., which can be used in various scenarios that need to control resource competition.
2. Clarify the overall structure of AQS
Take a look at the source code of AbstractQueuedSynchronizer in the juc package:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { //Head node private transient volatile Node head; //Tail node private transient volatile Node tail; //volatile modified semaphore private volatile int state; // Node of linked list static final class Node { volatile Node prev; volatile Node next; volatile Thread thread; } // ... } // AbstractQueuedSynchronizer parent class public abstract class AbstractOwnableSynchronizer implements java.io.Serializable { private transient Thread exclusiveOwnerThread; // ... }
AQS is mainly composed of the following components, the most important of which are semaphore stata and CHL queue
2.1 state semaphore
In the source code, state is modified by volatile keyword. In the previous study, we can know that the three characteristics of concurrent programming are atomicity, orderliness and visibility. The variable modified by volatile can ensure order and visibility.
volatile ensures visibility
- Write the state value, and the volatile feature will synchronize the data to the main memory
- Read the state value, and the volatile feature will reload data from the main memory into the working memory
This involves JMM. Unfamiliar readers can turn to my previous blog
The order of the instructions and the execution of the programs depends on the memory, so the order of the instructions and the code can be avoided.
The reason for adding volatile has been explained earlier, and then there is another feature, atomicity. Volatile can only guarantee the atomicity of a single variable, but can not guarantee the atomicity of a series of operations, so it is not thread safe. Then how can thread safety be guaranteed in AQS? Then how to implement it in the AQS source code? Turn down the source code and find two places:
Here is a very common CAS lock, which uses the unsafe api and the atomicity of cpu operation to ensure the atomicity of this operation
protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
This method directly sets a newStata, because it modifies a variable. When directly assigning values to variables of basic types, atomicity can be guaranteed
protected final void setState(int newState) { state = newState; }
2.2 FIFO queue
An AQS document given by AQS author Doug Lea: http://gee.cs.oswego.edu/dl/papers/aqs.pdf AQS is described in detail in, which can be read by readers with good English level
CHL queue is a core of AQS. FIFO queue, that is, first in first out queue. The main function of this queue is to store waiting threads. Assuming that many threads grab locks, most threads can't grab locks. Therefore, these threads need a queue to store them. This queue is CHL queue, which also follows FIFO rules.
The internal structure of CHL queue is a data structure of a two-way linked list. The basic component is the Node node. In the queue, head and tail are used to represent the head Node and tail Node respectively. Both of them point to an empty Node during initialization. The thread of the head Node can hold the lock, and then the subsequent threads queue for waiting, as shown in the figure
2.3,exclusiveOwnerThread
AbstractQueuedSynchronizer class extends AbstractOwnableSynchronizer class. AbstractOwnableSynchronizer has a thread object defined as exclusiveOwnerThread, which represents the holder of the synchronizer in exclusive mode.
3. AQS implementation principle
We have a clear understanding of the overall design of AQS. The main focus is on semaphore state and CHL queue, and then we can follow the source code implementation, @ see Java util. concurrent. locks. Abstractownablesynchronizer, you don't need to look at the source code a little. Just look at the main methods and classes and clarify the implementation principle of the framework. AQS mainly focuses on * acquire * and * release * these methods can be divided into two types: one is exclusive and the other is shared. Shared methods are added with a shared suffix
- Acquire, acquire shared: defines the logic of resource contention. If you don't get it, wait
- tryAcquire, tryacquishared: the actual operation that occupies resources is implemented by specific AQS users
- Release and releasedShared: define the logic of resource release. After the resource is released, adjust the queue and continue to fight for the subsequent nodes.
- tryRelease, tryrereleaseshared: the actual operation of resource release is implemented by AQS users
3.1 exclusive
Let's follow the AQS source code. Pay attention to the difference between exclusive and shared implementations
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
General idea of acquire method:
- The tryAcquire() method attempts to obtain resources. If successful, it returns directly
- If the acquisition fails, the current thread is wrapped as a Node and inserted into the tail of the CLH queue through the addWaiter method, and the exclusive thread is marked as Node EXCLUSIVE
- The acquirequeueueueued method blocks the thread in the waiting queue until the resource is obtained. In the whole waiting process, if the thread is interrupted, it returns true, otherwise it returns false. After the resource is obtained and interrupted, it will call selfInterrupt to make up the interruption
The tryAcquire method is implemented by a specific user class. If it is not implemented, an exception will be thrown, so it must be implemented
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
The addWaiter method is used to wrap the current thread as a Node and insert it into the tail of the CLH queue
private Node addWaiter(Node mode) { // Encapsulate the current thread as a Node node Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; // The tail node cannot be empty. The queue has been initialized if (pred != null) { // The front prev pointer of the Node node points to the tail Node node.prev = pred; // cas lock operation to ensure thread safety if (compareAndSetTail(pred, node)) { // The tail Node pointer points to the Node pred.next = node; return node; } } // The queue has not been initialized. Call enq method enq(node); return node; }
Follow the enq method
private Node enq(final Node node) { // Infinite loop, similar to while(true), spin operation for (;;) { // Tail node Node t = tail; if (t == null) { // Must initialize // Create a new node and set it as the head node if (compareAndSetHead(new Node())) // The head node is assigned to the tail node tail = head; } else { // If the tail Node is not empty, point the precursor of the Node node to the tail Node node.prev = t; // cas, set the node node as the tail node if (compareAndSetTail(t, node)) { // The next pointer of the tail Node points to the Node node t.next = node; return t; } } } }
Look up at Java util. concurrent. locks. Acquirequeueueued method of abstractqueuedsynchronizer #acquire method. As mentioned earlier, acquirequeueueueued method is used to queue threads until resources are obtained
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; // Spin, infinite cycle for (;;) { // Find node precursor node final Node p = node.predecessor(); // The precursor node is the head node, tryAcquire if (p == head && tryAcquire(arg)) { // The resource is obtained successfully. Set the node node to the new head node setHead(node); // The original head node is set to null and waits for GC garbage collection p.next = null; // help GC failed = false; return interrupted; } // p is not the head node, or tryAcquire() failed to obtain resources // shouldParkAfterFailedAcquire judge whether it can be park // parkAndCheckInterrupt determines whether park and Interrupt are successful if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
3.2 shared
Shared, that is, shared resources can be occupied by multiple threads at the same time
First look at the acquireShared method at the top level:
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
The tryAcquireShared method is implemented by a specific user class, so first look at the doAcquireShared source code:
private void doAcquireShared(int arg) { // Encapsulated as Node and marked as Node SHARED final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; // Infinite cycle, spin for (;;) { // Find precursor node final Node p = node.predecessor(); // Is it a head node if (p == head) { // Try to grab resources int r = tryAcquireShared(arg); if (r >= 0) { // Successful resource grabbing // Set as head node setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } // p is not the head node, or tryAcquire() failed to obtain resources // shouldParkAfterFailedAcquire judge whether it can be park // parkAndCheckInterrupt determines whether park and Interrupt are successful if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
This logic is basically similar to the exclusive type, except that the queued nodes are marked as SHARED shared, and tryacquiresshared is implemented by specific classes