1. Introduction to AQS
AQS (java.util.concurrent.locks.AbstractQueuedSynchronizer) is the basic framework class used to construct locks or other synchronization components (semaphores, events, etc.).The internal implementation of many concurrent tool classes in JDK relies on AQS, such as ReentrantLock, Semaphore, CountDownLatch, and so on.
The main way to use AQS is to inherit it as an internal auxiliary class to implement the synchronization primitive, which simplifies the internal implementation of your concurrent tools, blocking the underlying operations such as synchronization state management, thread queuing, waiting and waking.
In AQS-based synchronizer classes, the most basic operations include various forms of get and release operations.The get operation is a dependent state operation and is usually blocked.When using locks or semaphores, the meaning of the "get" operation is intuitive, that is, the lock or license is acquired, and the caller may wait until the synchronizer class is accessible.If a class wants to become a state-dependent class, it must have some states.AQS manages the state in the Synchronizer class, which manages an integer state information that can be manipulated using protected type methods such as getstate, setState, and compareAndSetState.This integer can be used to represent any state.
AQS mainly does three things:
- Management of Synchronization State
- Thread Blocking and Wakeup
- Maintenance of synchronization queues
Status Management API:
- int getState(): Get synchronization status
- void setState(): Set the synchronization state
- boolean compareAndSetState(int expect, int update): based on CAS, atomic setting state
AQS template method:
Method | describe |
---|---|
void acquire(int arg) | Get synchronization state exclusively. If the current thread gets synchronization state successfully, it is returned by this method. Otherwise, it will enter the synchronization queue and wait, which will call the overridden tryAcquire(intarg) method |
void acquireInterruptibly(int arg) | Same as acquire(int arg), but this method responds to interrupts, the current thread does not get the synchronization state and enters the synchronization queue. If the current thread is interrupted, this method throws InterruptedException and returns |
boolean tryAcquireNanos(int arg,long nanos) | A timeout limit is added to acquireInterruptibly(int arg), which returns false if the current thread does not get synchronization within the timeout and true if it does |
void acquireShared(int arg) | Shared fetch synchronization status, if the current thread does not get synchronization status, will enter the synchronization queue waiting, the main difference from exclusive fetch is that multiple threads can get synchronization status at the same time. |
void acquireSharedInterruptibly(int arg) | Like acquireShared(int arg), this method responds to interruptions. |
boolean tryAcquireSharedNanos(int arg,long nanos) | Timeout limits have been added to acquireSharedInterruptibly(int arg). |
boolean release(int arg) | Exclusive release synchronization state, which wakes up the thread contained by the first node in the synchronization queue after release synchronization state. |
boolean releaseShared(int arg) | Shared release synchronization state |
Collection getQueuedThreads() | Gets the collection of threads waiting on the synchronization queue |
Synchronizer overridable methods:
Method | describe |
---|---|
boolean tryAcquire(int arg) | Getting synchronization status exclusively requires querying the current status and determining if it is in the expected state before CAS sets the synchronization status. |
boolean tryRelease(int arg) | Exclusive release of synchronization state, threads waiting to get synchronization state will have the opportunity to get synchronization state |
int tryAcquireShared(int arg) | Shared Get Synchronization Status returns a value greater than or equal to 0 indicating success or failure |
boolean tryReleaseShared(int arg) | Shared Release Synchronization Status |
boolean isHeldExclusively() | Whether the current synchronizer is occupied by a thread in exclusive mode, which is a general way of indicating whether it is exclusive by the current thread |
Exposure methods for AQS:
2. AQS data structure
Since acquiring locks is conditional, threads that do not acquire locks will block waiting, and those waiting threads will be stored.The CLH queue is used in AQS to store these waiting threads, but it does not store the threads directly, it stores the node node that owns them.
2.1. Node data structure:
static final class Node { //Flag for shared mode, identifying a node waiting in shared mode static final Node SHARED = new Node(); //Marker for exclusive mode, identifying a node waiting in exclusive mode static final Node EXCLUSIVE = null; // The value of the waitStatus variable, which indicates that the thread was cancelled and that no locks will be acquired later static final int CANCELLED = 1; // The value of the waitStatus variable, which indicates that subsequent threads (that is, nodes behind this node in the queue) need to be blocked. (Used for exclusive locks) static final int SIGNAL = -1; // The value of the waitStatus variable, which indicates that the thread is waiting for blocking on the Condition condition. (await wait for Condition) static final int CONDITION = -2; // The value of the waitStatus variable indicates that the next acquireShared method thread should be propagated unconditionally.(for shared locks) static final int PROPAGATE = -3; // Marks the state of the current node. The default state is 0. States less than 0 have a special effect. States greater than 0 indicate cancellation. //SIGNAL: The successor node of this node is blocked, so the thread of the successor node needs to be replaced when the node releases a lock or interrupt. //CANCELLED: The node state is set to this state when the acquisition lock timeout or interruption occurs, and the node in this state will not be blocked again; //CONDITION: Identifies that this node is in a conditional queue and that this state does not occur on a node in a synchronous queue. //This state is set to 0 when a node moves from a conditional queue to a synchronous queue. //PROPAGATE: A releaseShared should be propagated to other nodes, this state is called in doReleaseShared(), //Ensure that propagation continues when other inserts occur. //Summary: A status of less than 0 means the node does not need to be notified to wake up; a status of 0 means the normal synchronization node; and CONDITION means the node is in //In the wait queue, the state is updated atomically by CAS. volatile int waitStatus; /** * Precursor node, set at enqueue, cleared when dequeue or precursor node cancels; */ volatile Node prev; /** * Subsequent nodes, set at enqueue, cleared when dequeue or predecessor node cancels; * The queuing operation does not set the successor nodes of the precursor node until the node is connected to the queue; * So a null next node does not necessarily mean that it is the end of the queue. When the next node is null, * prev nodes can be traversed for double-checking; the next s of canceled nodes point to themselves instead of null */ volatile Node next; //Threads owned by this node volatile Thread thread; /** * 1,Value is null or non-SHARED; null indicates exclusive mode; non-SHARED indicates queue waiting in Condition; * 2,The value is SHARED, indicating the shared mode; */ Node nextWaiter; //Is Shared Mode final boolean isShared() { return nextWaiter == SHARED; } //The precursor node of the current node, throws an NPE exception if the precursor node is null final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node() { // Used to establish initial head or SHARED marker } //Used in addWaiter(), to create nodes in a synchronization queue Node(Thread thread, Node mode) { // Used by addWaiter this.nextWaiter = mode; this.thread = thread; } //Used in Condition s, create a node to wait for a queue Node(Thread thread, int waitStatus) { // Used by Condition this.waitStatus = waitStatus; this.thread = thread; } }
Explain:
- waitStatus: Represents the status of the current node and has the following five states:
CANCELLED (1): When the acquisition of a lock is timed out or interrupted, the node state is set to this state, the node in this state is unpark, will not participate in acquiring the lock, will not be blocked again;
0: Represents the state of a normal node when it is initially inserted into a synchronization queue;
SIGNAL (-1): The successor node of this node is blocked, so the thread of the successor node needs to be replaced when the node releases a lock or interrupt.
CONDITION (-2): Identifies that this node is in a conditional queue and that this state does not occur at the node in the synchronization queue, which is set to 0 when the node moves from the conditional queue to the synchronization queue;
PROPAGATE (-3): A release Shared should be propagated to other nodes, and this state is called in doReleaseShared() to ensure that the propagation continues on other inserts.
Summary: A status of less than 0 means that the node does not need to be notified to wake up; a status of 0 means a normal synchronization node; and a CONDITION means that the node is in a waiting queue and the state is updated atomically through CAS.
- prev: the precursor node, set at enqueue, cleared when dequeue or precursor node cancels;
- Next: the succeeding node, set at enqueue, cleared when dequeue or precursor node cancels; the queuing operation does not set the succeeding node of the precursor node until the node is connected to the queue; therefore, a null next node does not necessarily mean that the node is at the end of the queue; when the next node is null, the prev node can be traversed for double checking; the next of the canceled node points to itself instead of null
- **nextWaiter:**Value is SHARED, indicating shared mode; value is null or non-SHARED, indicating exclusive mode when null, and node waiting in Condition queue when non-SHARED;
WatStatus status:
2.2, AQS data structure
/** * Wait for the head node of the queue to be updated by the setHead method; the state of the head node cannot be CANCELLED */ private transient volatile Node head; /** * Waiting for the end node of the queue */ private transient volatile Node tail; /** * Synchronization status */ private volatile int state;
3. CLH Queue Related Operations
3.1. CAS operations for related attributes
/** * Setting the head value of AQS through CAS */ private final boolean compareAndSetHead(Node update) { return unsafe.compareAndSwapObject(this, headOffset, null, update); } /** * Setting tail value of AQS through CAS */ private final boolean compareAndSetTail(Node expect, Node update) { return unsafe.compareAndSwapObject(this, tailOffset, expect, update); } /** * Set waitStatue value of Node node through CAS */ private static final boolean compareAndSetWaitStatus(Node node, int expect, int update) { return unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update); } /** * Setting the next value of Node node through CAS */ private static final boolean compareAndSetNext(Node node, Node expect, Node update) { return unsafe.compareAndSwapObject(node, nextOffset, expect, update); }
3.2, Add Node to CLH End
//Insert a node into the end of the synchronization queue by idling and CAS private Node enq(final Node node) { for (; ; ) { Node t = tail; //If the tail node is empty, CAS initializes the header node directly and sets the tail node as the header node if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; //If the tail node is not empty, CAS sets the current node as the tail node if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
3.3. Add the current thread to the end of the CLH queue
//Creates a node of a given pattern with the current thread and joins the node to the end of the queue private Node addWaiter(Node mode) { //Create Threads Node node = new Node(Thread.currentThread(), mode); //Fast Detection Tail Node is not empty, CAS replaces the current node with the tail node Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //Fast replacement failed?Then insert the current node into the end of the queue by emptying enq() enq(node); return node; }
4. Exclusive Lock
Exclusive locks have two main functions:
- The ability to acquire locks: Since only one thread can acquire locks when multiple threads acquire locks together, other threads must block waiting at the current location.
- Function to release locks: Threads that acquire locks release lock resources and must be able to wake up a thread that is waiting for a lock resource.
4.1. Exclusive Lock Acquisition Process
4.2. Approaches to acquiring exclusive locks
//Acquire exclusive locks, ignoring interrupts; this method is often called by lock.lock until a lock is successfully acquired public final void acquire(int arg) { //tryAcquire: CAS tries to acquire the lock first, and when true is returned it indicates success, this method is implemented by subclasses; //When false is returned, it is called //acquireQueued handles acquisition and queue blocking and wakes up altered threads when other threads release locks. if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
//Call tryAcquire to attempt to acquire the lock, queue the thread node and block the node thread if the acquisition fails; //Until the thread is interrupted or waked up, it will try to acquire the lock again; final boolean acquireQueued(final Node node, int arg) { //Identity acquisition lock failed boolean failed = true; try { //Identity lock interrupted boolean interrupted = false; for (; ; ) { //Gets the precursor node of the current node, and if the precursor node is the head node, attempts to acquire the lock; //Set the current node as the head node if successful, otherwise try to block the node thread final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { //Set the current node as the head node setHead(node); p.next = null; // help GC failed = false; return interrupted; } //Determines whether the blocking condition is currently satisfied or blocks the current thread if it is satisfied; //And wait for interruption or wakeup if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { //Cancel acquisition if lock acquisition fails if (failed) cancelAcquire(node); } }
//Determine if the current node thread needs to be blocked private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //Status of the precursor node int ws = pred.waitStatus; //The state of the precursor node is SIGNAL, indicating that the precursor node is waiting for a signal to acquire a lock //So this node can be safely blocked if (ws == Node.SIGNAL) return true; //The precursor node waitStatus>0, that is, waitStatus=CANCELLED; //Indicates that the precursor node has been cancelled and needs to be traversed forward until it is in state //Not a node of CANCELLED, and set this node as the precursor node of the node; //Returns false, letting the upper call continue trying to acquire the lock if (ws > 0) { //Loop through the precursor nodes to find nodes that are not in the CANCELLED state and set to the current node's //Precursor Node do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //Setting the precursor node state to SIGNAL through CAS when the current precursor node state is 0 or PROPAGATE //And return to fase, waiting for the next loop to block the current node thread; compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } //Block the current thread through LockSupport.park() until it is unpark ed or interrupted private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
4.3. Release exclusive lock related methods
//Release exclusive lock public final boolean release(int arg) { //Call tryRelease to attempt to release the lock via CAS, which is implemented by subclasses if (tryRelease(arg)) { Node h = head; //Head Node is not empty and Head Node Status is not zero, should be SIGNAL //Represents a node in the queue that needs to be waked up, and calls unparkSuccessor for a header node thread //Wake-up operation if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
//Wake up node thread processing private void unparkSuccessor(Node node) { //Get the current node state int ws = node.waitStatus; //If the state is less than zero, the state is reset to zero, indicating that node processing is complete if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ //Gets the successor node when the successor node is empty or the successor node state is CANCELLED; //Traverse the queue forward by tail to find the next valid node of the current node, waitStatus <= 0 //Node Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } //Next node is not empty, representing a node waiting for a signal, executing unpark wake-up node thread if (s != null) LockSupport.unpark(s.thread); }
5. Shared Lock
5.1. Shared lock acquisition process
5.2. Shared lock acquisition related methods
//Acquire shared lock, ignore interrupt; public final void acquireShared(int arg) { //Subclass implementation, CAS method to acquire shared locks, if acquisition fails, call doAcquireShared to continue acquiring shared locks if (tryAcquireShared(arg) < 0) //Attempt to acquire shared lock, failure to acquire queues current thread node until notified or interrupted doAcquireShared(arg); }
//Acquire shared locks, if CAS acquisition fails, queue the current node and block the current thread until locks are acquired private void doAcquireShared(int arg) { //Insert the current node at the end of the queue final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (; ; ) { //Gets the precursor node of the current node, and if the precursor node is the head node, attempts to acquire the lock; //Check node state if acquisition fails; block node thread when node state is SIGNAL final Node p = node.predecessor(); if (p == head) { //CAS Acquire Lock int r = tryAcquireShared(arg); if (r >= 0) { //Success sets the current node as the head node and other node states as PROPAGAE setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } //Check if the current node should be blocked, or block until it is interrupted if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { //Cancel acquisition if lock acquisition fails if (failed) cancelAcquire(node); } }
//Set current node as head node and wake up threads in shared mode private void setHeadAndPropagate(Node node, int propagate) { Node h = head; //Set as Head Node setHead(node); //If propagate > 0 or header node is empty and header node state is < 0 if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; //Get the header node if its successor is in shared mode if (s == null || s.isShared()) doReleaseShared(); } }
5.3. Shared lock release related methods
//Release shared lock public final boolean releaseShared(int arg) { //cas releases locks, doReleaseShared releases locks if it fails if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
private void doReleaseShared() { for (; ; ) { //When getting the header node, the header node is not empty and the state is SIGNAL, CAS sets the state to 0 and wakes the thread //Otherwise, set the header node state to PROPAGATE, then cycle through the header node state and try to wake up Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }
6. Conditions
6.1. Implementation of condition
First, there is a Condition queue internally that stores all the threads waiting for this Condition condition.
await family method: let the thread currently holding the lock release the lock, wake up a thread waiting for the lock on the CLH queue, create a node node for the current thread, and insert it into the Condition queue (note that it is not inserted into the CLH queue)
signal family method: Instead of waking any threads here, wait nodes on the Condition queue are inserted into the CLH queue, so when the thread holding the lock finishes executing the release of the lock, one of the threads in the CLH queue is waked up, and then the thread is waked up.
6.2, await and signal processing
6.3. await related methods
//Block the thread currently holding the lock, wait, and release the lock.If there is an interrupt request, an InterruptedException exception is thrown public final void await() throws InterruptedException { //Throw an interrupt exception if the current thread has been interrupted if (Thread.interrupted()) throw new InterruptedException(); // Create a new Node node for the current thread and insert it into the Condition queue Node node = addConditionWaiter(); //Release locks held by the current thread and wake up the header node in the synchronization queue int savedState = fullyRelease(node); int interruptMode = 0; //If the current node fills the synchronization queue; //Blocks the current thread, which joins the current node to the synchronization queue after it is awakened by a signal signal; //Waiting to acquire lock while (!isOnSyncQueue(node)) { LockSupport.park(this); //Check if interrupted and queued for locks if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } // If the node node node is already in the synchronization queue, the synchronization lock is acquired, and the execution can continue only if the lock is acquired, otherwise the thread continues to block and wait if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; // Clear nodes in Condition queue whose state is not Node.CONDITION if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); // Do you want to throw an exception or issue an interrupt request if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }
//Create a new Node node for the current thread and insert it into the Condition queue private Node addConditionWaiter() { Node t = lastWaiter; // If the wait queue tail node state is not CONDITION, the clean up operation is performed; // Clean up nodes in queue whose state is not CONDITION if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); t = lastWaiter; } //Create a node with CONDITION status for the current thread and insert the node at the end of the queue Node node = new Node(Thread.currentThread(), Node.CONDITION); if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node; }
//Traverse through the waiting queue from beginning to end, removing nodes whose state is not CONDITION private void unlinkCancelledWaiters() { //Record the next node to be processed Node t = firstWaiter; //Log the previous node with CONDITION status Node trail = null; while (t != null) { Node next = t.nextWaiter; if (t.waitStatus != Node.CONDITION) { t.nextWaiter = null; if (trail == null) firstWaiter = next; else trail.nextWaiter = next; if (next == null) lastWaiter = trail; } else trail = t; t = next; } }
//Release lock held by current thread and wake up synchronization queue a waiting thread //If it fails, throw an exception and set the node's state to Node.CANCELLED final int fullyRelease(Node node) { boolean failed = true; try { int savedState = getState(); //Release locks held by the current thread if (release(savedState)) { failed = false; return savedState; } else { throw new IllegalMonitorStateException(); } } finally { if (failed) node.waitStatus = Node.CANCELLED; } }
//Determine whether a node is released in a synchronization queue final boolean isOnSyncQueue(Node node) { // If the status of a node is Node.CONDITION, or if the node does not have a previous node prev, // Then return false, the node node is not in the synchronization queue if (node.waitStatus == Node.CONDITION || node.prev == null) return false; //If a node has the next node, it must be in the synchronization queue if (node.next != null) // If has successor, it must be on queue return true; //Find node from Synchronization Queue return findNodeFromTail(node); }
//Based on the current pattern, determine whether to throw an exception or break again, etc. private void reportInterruptAfterWait(int interruptMode) throws InterruptedException { if (interruptMode == THROW_IE) throw new InterruptedException(); else if (interruptMode == REINTERRUPT) selfInterrupt(); }
6.4. signal-related methods
//If the wait queue is not empty, insert the queue header node into the synchronization queue public final void signal() { //Throw an exception if the current thread is not an exclusive lock if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; //Insert the header node in the waiting queue into the synchronization queue if (first != null) doSignal(first); }
//Insert the header node waiting for the total queue into the synchronous queue private void doSignal(Node first) { do { // The original Condition queue header node was cancelled, so the Condition queue header node was reassigned // If the new Condition queue header node is null, the Condition queue is empty // So also set the last Waiter at the end of the Condition queue to null if ((firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; } while (!transferForSignal(first) && (first = firstWaiter) != null); }
// Returning true indicates that the node node is inserted in the synchronization queue, returning false indicates that the node is not inserted in the synchronization queue final boolean transferForSignal(Node node) { //If the node state cannot be modified from CONDITION to 0, the node is already in the synchronization queue and returns false directly if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; //Inserts the node node node into the synchronization queue, p is the original synchronization queue end node and the previous node of the node Node p = enq(node); int ws = p.waitStatus; // If the previous node is in a canceled state, or it cannot be set to a Node.SIGNAL state. // This means that node p will not wake up the next node thread. // So call the LockSupport.unpark(node.thread) method directly here to wake up the thread on which the node node is located if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; }
//Wake up all nodes public final void signalAll() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignalAll(first); }
//Loop wakes up all waiting nodes private void doSignalAll(Node first) { lastWaiter = firstWaiter = null; do { Node next = first.nextWaiter; first.nextWaiter = null; transferForSignal(first); first = next; } while (first != null); }