AQS Detailed Exclusive Lock Mode

Introduction to AQS

AbstractQueuedSynchronizer is short for AQS, or Queue Synchronizer.It is the core component under the JUC package and its main usage is inheritance. Subclasses manage synchronization state by inheriting AQS and implementing its abstract method, which is divided into exclusive locks and shared locks.Many synchronization components are based on it, such as our common ReentrantLock, which is based on AQS exclusive locks, which means that only one thread can hold a lock at a time.For example, ReentrantReadWriteLock is implemented based on AQS shared locks, which allow multiple threads to acquire locks simultaneously and access resources concurrently.AQS is a two-way FIFO queue built on CAS that maintains an int-type state decorated with volatile s to ensure a secure state row.
AQS provides three ways to change state:

  1. getState(): Returns the current value of the synchronization state
  2. setState(): Sets the current synchronization state
  3. compareAndSetState(): Sets the current state using CAS, which guarantees the atomicity of the state.It is guaranteed by the native method in the Unsafe class.

AQS Principle

If the requested shared resource is idle, set the currently requested thread as a worker thread and the shared resource as locked.If the requested shared resource is used, a set of mechanisms for allocating locks that are blocked, waited, and awakened by threads is required.Then this mechanism, AQS, is implemented with CLH queue locks, and threads that cannot acquire locks will join the queue.A synchronization queue maintained internally by AQS. Threads that get failed join the queue to spin. Removing the queue requires that the precursor node is the head node and the synchronization state is successfully obtained. Releasing the synchronization state AQS calls the unparkSuccessor method to wake up subsequent nodes.

AQS data structure

The internal maintenance of the AQS queue is a FIFO two-way Chain table, as illustrated below.The feature of this structure is that each data structure has two pointers, which point to the direct precursor node and the direct successor node.This structure allows easy access to precursor and successor nodes from any one node.Each Node is encapsulated by a thread and joins the AQS queue when the competition fails.

Let's take a closer look at Node composition:

static final class Node {
    /** Indicates that the node is waiting for tags in shared mode */
    static final Node SHARED = new Node();
    /** Wait flag indicating that the node is in exclusive lock mode */
    static final Node EXCLUSIVE = null;
    /** waitStatus Value indicating thread cancellation */
    static final int CANCELLED =  1;
    /** waitStatus Value indicating that the thread needs to suspend */
    static final int SIGNAL    = -1;
    /** waitStatus Value indicating that the thread is waiting*/
    static final int CONDITION = -2;
    /**waitStatus Value indicating that the next shared mode should propagate unconditionally*/
    static final int PROPAGATE = -3;
    /**Status field*/
    volatile int waitStatus;
    /**Precursor Node */
    volatile Node prev;
    /**Successor Node */
    volatile Node next;
    /**Current Thread*/
    volatile Thread thread;
    /**Threads that column this node to take over the next node*/
    Node nextWaiter;
    /**Returns true if the node is waiting in shared mode*/
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    /**Return to the previous node, throw an exception if null, precursor node is not used by null */
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
    Node() {    // Used to create an initialization head node
    }
    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }
    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

AQS Add Node

The AQS process diagram for joining nodes to the synchronization queue is as follows:

The process of joining the queue must be thread-safe, so AQS provides a method based on CAS to set the tail node, compareAndSetTail, which is also a native method in the unsafe class.It needs to pass in what the current thread considers the tail node and the current node, and when set successfully, the current node and the tail node are associated and the current node formally joins the queue.

Important AQS Methods

AQS uses the template method mode, and the custom synchronizer needs to override the following template methods provided by AQS:

isHeldExclusively()//Whether the thread is in an exclusive resource.Conditions are the only ones that need to be implemented.
tryAcquire(int)//Exclusive access to resources, success returns true, failure returns false
tryRelease(int)//Exclusive release of resources, success returns true, failure returns false
tryAcquireShared(int)//Share access to resources.Negative numbers indicate failure, 0 indicates success but no remaining resources are available; positive numbers indicate success and have remaining resources
tryReleaseShared(int)//Sharing frees resources. Returns true successfully and false for failure.

AQS Exclusive Lock Mode

The acquisition of exclusive locks is acquire() provided through AQS.Let's take a look at the source code for this method:

public final void acquire(int arg) {
   if (!tryAcquire(arg) &&
       acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
       selfInterrupt();
}

Find out if acquire() did two things to get the synchronization status successfully or not.1 Success, method end return, 2 failure, will join the current thread to the synchronization queue by calling addWaiter() and acquireQueued() methods. Let's continue to look at the source code of these two methods.

private Node addWaiter(Node mode) {
   Node node = new Node(Thread.currentThread(), mode);
   Node pred = tail;
   if (pred != null) {
       node.prev = pred;
       if (compareAndSetTail(pred, node)) {
           pred.next = node;
           return node;
      }
  }
   enq(node);
   return node;
}

The method discovers that it encapsulates the current thread as a Node type, then determines if the tail node is empty, if CAS operations are not empty and queued, if they are empty, enq() is called, which inserts the node by spinning the CAS tail through a continuous for loop.

Now that we understand how exclusive lock acquisition fails to enter the queue, what can we do to synchronize the nodes in the queue to ensure that we have the opportunity to acquire exclusive locks?Let's take a look at the source code for the acquireQueued() method

final boolean acquireQueued(final Node node, int arg) {
   boolean failed = true;
   try {
       boolean interrupted = false;
       for (;;) {
           final Node p = node.predecessor();//Get Precursor Nodes
           if (p == head && tryAcquire(arg)) {//If the current node is the head node and the synchronization state is successfully acquired, the lock is acquired
               setHead(node);                 
               p.next = null; // help GC
               failed = false;
               return interrupted;
          }
           if (shouldParkAfterFailedAcquire(p, node) &&
               parkAndCheckInterrupt())//Get the method of the failed call
               interrupted = true;
      }
  } finally {
       if (failed)
           cancelAcquire(node);
  }
}

From the source code, we can see that this is a spin process (for(;)). It first obtains the precursor node of the current node, and then determines whether the current node can acquire exclusive locks. If the precursor node is the head node and obtains the synchronization state, the exclusive locks can be acquired.If the acquisition of a lock fails, the thread goes into a wait state and waits to acquire an exclusive lock.

shouldParkAfterFailedAcquire() The main logic of this method is to call compareAndSetWaitStatus(), which uses CAS to set the node state from INITIAL to SIGNAL.If a failure returns false, the autorotation through acquireQueued() will continue until it succeeds.After successful setup, the parkAndCheckInterrupt() method is called, which calls LockSupport.park(this) to block the thread.The exclusive lock acquisition process has been analyzed.

AQS Exclusive Lock Acquisition Flowchart

Exclusive Lock Release

Exclusive lock is released using relase() method, let's take a look at the source code

public final boolean release(int arg) {
   if (tryRelease(arg)) {
       Node h = head;
       if (h != null && h.waitStatus != 0)
           unparkSuccessor(h);
       return true;
  }
   return false;
}

The logic of this code is easy to understand. If the synchronization state is released successfully, the code in the if statement is executed, the unparkSuccessor() method is executed when the head er is not empty and the state is not 0, and the LookSupport.unpark() method is executed by the unparkSuccessor method. Each release of a lock wakes up a subsequent node of that node in the queue, which further explains that acquiring a lock is aFIFO process.

More concurrent content please pay attention to WeChat Public Number programmer's deep-sea exploration


 

Nineteen original articles were published, 10 were praised, 10,000 visits+
Private letter follow

Posted on Sun, 02 Feb 2020 20:01:31 -0800 by Jakebert