AQS queue synchronizer
Build a framework for locks and synchronizers. It can be used by mutex, retrantLock, etc
effect
Many methods for obtaining and releasing synchronization state are defined to provide synchronization components, which are generally used to be inherited. Some methods are provided, such as getstate, setstate, compareandsetstate
Share and exclusive are supported.
Some methods that need to be rewritten are provided, and then the synchronizer can be combined and implemented in a custom synchronization component. The rewriting methods are
- tryActuire: exclusively obtain the synchronization status and judge whether it meets the expectation,
- tryRelease: exclusively obtain the release status,
Method classification provided by template
-
Exclusively acquire and release synchronization state
-
Share get and release synchronization status
-
Query the waiting threads in the synchronization queue.
Template method provided:
structural analysis
AbstractQueuedSynchronizer (hereinafter referred to as AQS) is an abstract class that defines some general functions and methods that subclasses need to rewrite according to their own characteristics. It has two internal classes Node and ConditionObject, and two queues with linked list structure,
-
Sync queue, that is, synchronization queue, is a two-way linked list, including head node and tail node. The head node is mainly used for subsequent scheduling. The store attempted to acquire a lock and failed to acquire a waiting thread
-
Condition queue is not required. It is a one-way linked list. This one-way linked list exists only when condition is used. And there may be multiple condition queues. It is used to store threads that have obtained locks and are waiting for certain events (such as IO events, mq messages, etc.) * actively give up the lock and suspend the waiting conditions *.
Therefore, the synchronization queue is waiting to obtain the lock. The thread in the condition queue is waiting for the condition. It actively calls the await method and is put into the condition queue after actively releasing the lock.
CLH(Sync queue)
CLH: virtual bidirectional queue. There are only associations between nodes. AQS encapsulates the resource thread shared by each request into a node of CLH lock queue.
There are two internal classes, Node class and ConditionObject class
Node
The internal Node represents a Node. The Node state implementation method is volatile+CAS
static final class Node { /** Marker to indicate a node is waiting in shared mode */ static final Node SHARED = new Node(); /** Marker to indicate a node is waiting in exclusive mode */ static final Node EXCLUSIVE = null; /** waitStatus value to indicate thread has cancelled */ static final int CANCELLED = 1; /** waitStatus value to indicate successor's thread needs unparking */ static final int SIGNAL = -1; /** waitStatus value to indicate thread is waiting on condition */ static final int CONDITION = -2; /** * waitStatus value to indicate the next acquireShared should * unconditionally propagate */ static final int PROPAGATE = -3; private transient volatile Node head; private transient volatile Node tail; /** CANCELLED,A value of 1 indicates that the current thread is canceled. After being interrupted, you no longer need to wait for resources in the queue, and you need to perform node out of queue operations SIGNAL,A value of - 1 indicates that the threads contained in the subsequent nodes of the current node need to be run and unpark CONDITION,A value of - 2 indicates that the current node is waiting for the condition and is in the condition queue PROPAGATE,A value of - 3 indicates that subsequent acquiresared in the current scene can be executed The default value is 0, which means that the current node is in the sync queue waiting to acquire the lock. */ volatile int waitStatus; // Precursor node volatile Node prev; // Successor node volatile Node next; // Thread reference corresponding to node volatile Thread thread; // Next waiting person Node nextWaiter; ... // Whether the node is waiting in sharing mode final boolean isShared() { return nextWaiter == SHARED; } } /** The shared variable of the whole class represents the synchronization status. CAS is used internally to change this value */ private volatile int state; /** * Modification of status: */ protected final boolean compareAndSetState(int expect, int update) { // See below for intrinsics setup to support this return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
Status of state
state has the following types:
- SIGNAL
Signal: blocked (via Park), so the current node must unpark its success, followed by when it releases or cancels To avoid races, acquire methods must first indicate they need a signal, then retry the atomic acquire, and then, on failure, block. After the current node thread uses the synchronization resources and releases the resources, it needs to wake up the thread in the next node
- CANCELLED
This node is cancelled due to timeout or interrupt. a thread with cancelled node never again blocks. Need to remove from queue
- CONDITION
ObjectCondition class
It inherits some methods of the Condition interface
Defines the methods of waiting and notification. Because the thread inside spontaneously calls await to release the lock because it triggers an interrupt for some reason.
condition is a lock that depends on the current.
await(): the current process joins the waiting Condition queue and releases the lock
signal(): wake up the node with the longest waiting time in the queue, and move the node to the synchronization queue before waking up.
// Some interfaces in Condition public interface Condition { // Wait, the current thread is waiting until it receives a signal or is interrupted void await() throws InterruptedException; // Wait. The current thread is in a waiting state until it receives the signal and does not respond to the interrupt void awaitUninterruptibly(); //Wait. The current thread is in a waiting state until it receives a signal, is interrupted, or reaches the specified waiting time long awaitNanos(long nanosTimeout) throws InterruptedException; // Wait. The current thread is in a waiting state until it receives a signal, is interrupted, or reaches the specified waiting time. This method is behaviorally equivalent to: awaitnanos (unit. Tonanos (time)) > 0 boolean await(long time, TimeUnit unit) throws InterruptedException; // Wait, the current thread is waiting until it receives a signal, is interrupted, or reaches a specified deadline boolean awaitUntil(Date deadline) throws InterruptedException; // Wake up a waiting thread. If all threads are waiting for this condition, select one of them to wake up. Before returning from await, the thread must re acquire the lock. void signal(); // Wake up all waiting threads. If all threads are waiting for this condition, all threads wake up. Before returning from await, each thread must re acquire the lock. void signalAll(); }
Overview of blocking methods:
... while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } ...
Wake up:
// Wake up the first process public final void signal() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignal(first); } private void doSignal(Node first) { do { if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; } while (!transferForSignal(first) && // //Failed to transfer the node from the condition queue to the sync queue, and the head node in the condition queue is not empty, and the loop continues (first = firstWaiter) != null); } // If the status modification fails, it returns. However, doSignal will be called repeatedly until the modification is successful final boolean transferForSignal(Node node) { /* * If cannot change waitStatus, the node has been cancelled. */ if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; /* * Splice onto queue and try to set waitStatus of predecessor to * indicate that thread is (probably) waiting. If cancelled or * attempt to set waitStatus fails, wake up to resync (in which * case the waitStatus can be transiently and harmlessly wrong). */ Node p = enq(node); int ws = p.waitStatus; if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; }
acquire (for exclusive resources)
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
This method obtains (resources) in exclusive mode and ignores interrupts, that is, it is invalid to interrupt this thread during acquire
-
First, call tryAcquire to try to obtain the object state in exclusive mode. The implementation method is determined by the subclass (for example, ReentrantLock has two methods: fair and unfair)
-
If it cannot be obtained, use the addWaiter method to encapsulate the thread as a Node and add a Node at the end of the sync queue. The rotation training mode is added to the end of the queue until it is successful.
-
Call the acquirequeueueueued method to return whether the thread is interrupted. If it is found that the front node of the node is head after adding the node in the previous step, it will try to obtain the lock resource again. If it has been released, the (thread) will obtain the lock for execution. If it is not the head node or the attempt to obtain the lock resource fails, it will find a place in the queue to wait. At this time, you need to find a safe point to ensure that after the thread of the previous valid node (the node that has not been cancelled) executes, it can notify to wake itself up, and the current node thread can hang up and wait at ease. In addition, set the state of the front node to SIGNAL
final boolean acquireQueued(final Node node, int arg) { //The default is failure boolean failed = true; try { //The default is not interrupted boolean interrupted = false; //Infinite loop for (;;) { final Node p = node.predecessor(); //1. If the front node is the head node, try to acquire the lock again. If the acquisition is successful, update yourself to head. At this time, the current thread can execute. if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; // At this time, false is returned without interruption return interrupted; } //2.1 if the failure or the front is not the head, then you need to locate an effective location to block the waiting. Some of the previous nodes may be cancelled. You need to skip these nodes and clear the queue. shouldParkAfterFailedAcquire is to do this if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //3. Mark the interrupt flag and return the interrupt interrupted = true; } } finally { // 4. Under normal circumstances, the for loop will always be carried out. When the loop jumps out, an exception occurs. If an interrupt is marked, call blocking and throw an interrupt exception. At this time, no safe position is found in the queue, so move the node out of the queue if (failed) cancelAcquire(node); } }
Release
public final boolean release(int arg) { if (tryRelease(arg)) { // Release successful // Save header node Node h = head; if (h != null && h.waitStatus != 0) // The header node is not empty and the header node status is not 0 unparkSuccessor(h); //Release the successor node of the header node return true; } return false; }
tryRelease needs to be implemented by subclasses.
Reference
1 juc source code
4 the art of Java Concurrent Programming