J.U.C|AQS Exclusive Source Code Analysis

Keywords: Java REST less

I. Write in front

In the last article, we talked about the AQS architecture and its implementation principle through the process of locking and unlocking of ReentrantLock. Principles of J.U.C|AQS.

Understanding the principle, let's see how the source code is implemented step by step.

This chapter talks about the exclusive process of acquiring and releasing shared state in AQS, mainly according to tryAcquire (int arg) - - > tryRelease (int arg).

2. What is monopoly

AQS synchronous queues provide two modes: EXCLUSIVE and SHARED.

In this chapter, we mainly talk about monopoly: only one thread can get synchronization status at the same time, and other threads that fail to get synchronization status will join the synchronization queue and wait.

Main explanatory methods:

  • tryAcquire(int): Monopoly. Attempts to acquire resources return true for success and false for failure.
  • tryRelease(int): Monopoly. Attempts to release resources return true for success and false for failure.

See if you don't understand synchronization queues J.U.C | Synchronization Queue (CLH)

3. Analysis of Core Methodologies

3.1 Acquisition of Shared State

acquire(int arg)

The top-level access acquisition (int arg) method for exclusive access to synchronous state returns directly if the thread gets the shared state, otherwise the current thread is constructed as an exclusive (node.EXCLUSIVE) mode node and added to the tail of the synchronous queue until the shared state is acquired, and the whole process ignores the interruption.

Method source code

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

Method function:

  • tryAcquire(arg): Attempts to obtain synchronization status and success are returned directly.
  • addWaiter(Node.EXCLUSIVE): When synchronization status acquisition fails, build an exclusive node and add it to the end of the synchronization queue.
  • AcquireQueued (Node, arg): Gets the specified number of resources of the node, spins until it succeeds, and returns the interrupted state of the thread of the node.
  • selfInterrupt(): The interrupt is filled (because the entire process of obtaining resources ignores the interrupt, the interrupt is finally filled manually)

    Source code analysis

tryAcquire(arg)

protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

??? What the hell? Throw an exception directly? AQS does not provide a specific implementation for the acquisition of shared state, waiting for subclasses to be implemented according to their own scenarios. Does anyone wonder why it's not abstract's ni? Because AQS is not only a lock of exclusive mode, it needs to be inherited by others as well. We can't let others realize an unrelated method.

addWaiter(Node node)

private Node addWaiter(Node mode) {
// Modes have two mode s for building nodes in a given pattern 
//  Shared SHARED, Exclusive EXCLUSIVE;
  Node node = new Node(Thread.currentThread(), mode);
    // Attempt to quickly add the node to the end of the queue
    Node pred = tail;
     if (pred != null) {
        node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // If the fast join fails, it is enrolled by anq
        enq(node);
        return node;
    }

The addWaiter(Node mode) method attempts to quickly add the current Node node to the end of the queue, and if the fast join fails, it spins in through the enq(node) method.

enq(final Node node)

private Node enq(final Node node) {
// CAS spins until the end of the team is successful        
for (;;) {
    Node t = tail;
        if (t == null) { // If the queue is empty, you must first initialize the CLH queue, create a new empty node ID as the Hader node, and point tail at it.
            if (compareAndSetHead(new Node()))
                tail = head;
            } else {// Normal process, join queue tail
                node.prev = t;
                    if (compareAndSetTail(t, node)) {
                        t.next = node;
                        return t;
                }
            }
        }
    }

The enq(final Node node) method adds the current Node node to the end of the queue by spinning until it succeeds.

Note: In this way, whether fast or spin, the current Node node is added to the end of the queue by compareAndSetTail(t, node) to ensure thread safety, which is also a typical way to achieve lock-free thread safety, CAS spin volatile variable.

acquireQueued(final Node, int arg)

final boolean acquireQueued(final Node node, int arg) {
    // Are resources available?
    boolean failed = true;
        try {
            // Has the marker been interrupted while waiting
            boolean interrupted = false;
            // spin
           for (;;) {
            // Get the precursor node of the current node
           final Node p = node.predecessor();
           // If its predecessor node is the head node, this node is qualified to obtain resources. (Maybe waken up by the precursor node, or interrupted)
          if (p == head && tryAcquire(arg)) {
            // When you get the resources, set yourself to the head node.
            setHead(node);
           // The precursor node p.next = nul is set Head (node); node.prev = null has been set to be empty to facilitate GC to recycle the precursor node, which is also equivalent to listing.
          p.next = null; // help GC
         failed = false;
         return interrupted;
        }
    // If you don't meet the above conditions, you can rest and wait until unpark()
        if (shouldParkAfterFailedAcquire(p, node) &&
            parkAndCheckInterrupt())
         interrupted = true;
     } finally {
        if (failed)
            cancelAcquire(node);
     }
}

Threads of the current node try to get synchronization status in the'dead cycle', provided that only its predecessor node is the head node is eligible to try to get synchronization status, otherwise they continue to wait for wake-up in the synchronization queue.

Why?

  • Because only the head is the node that successfully acquires the synchronization state, while the thread of the head node releases the synchronization state, it wakes up the succeeding node. After waking up, the succeeding node detects whether its predecessor node is the head node or not, and if so, it tries to obtain the synchronization state by spinning.
  • Maintain FIFO principles of CLH. In this method, the node spins to obtain the synchronization state.

Following chart

shouldParkAfterFailedAcquire(Node pred, Node node)

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // Get the status of the pioneer
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
           // If you have already told the precursor node, after getting the resources to inform yourself, then you can rest at ease.
            return true;
        if (ws > 0) {
           // If the predecessor node gives up, it loops forward until it finds a node in its normal waiting state, behind it.
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
        // If the predecessor state is 0 or PROPAGATE, set the predecessor state to SIGNAL and tell it to notify itself after obtaining resources.
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

This method detects whether the head node is in its predecessor node, if it is trying to get the synchronization state, or if it is not, it goes back to the synchronization queue again to find a comfortable place to rest (that is, to find a node with waitStatus > 0, and to wait behind him) and tells the predecessor node to release the synchronization state or to notify itself after being interrupted (compare AndSetWaitStatus,pred, Ws, Node. SIGNAL).

Note: When looking for a waitStatus > 0 node, those unqualified nodes will form an invalid chain waiting for GC recovery.

private final boolean parkAndCheckInterrupt() {
        // Calling the park method is that the thread enters the waiting state
        LockSupport.park(this);
        //If waked up to see if it was interrupted?
        return Thread.interrupted();
    }

Finally, the park method is invoked to wating the threads in the node, waiting to be awakened by unpark().

Summary

  1. The request thread first calls the tryAcquire(arg) method to try to get the synchronization status, and returns directly if it succeeds.
  2. If it fails:
  • Constructing an exclusive node Node.EXCLUSIVE
  • AddiWaiter (Node. EXCLUSIVE) tries to quickly add the node to the end of the queue, and returns it directly to the end of the queue if it succeeds. When it fails, it calls the enq(final Node node) method to add the node to the end of the queue using spin CAS.
  1. Call the acquireQueued (final Node, int arg) method to find a comfortable rest area and notify the precursor node to wake up after releasing the synchronization state or being interrupted to retrieve the synchronization state from a new attempt.
  2. Finally, if the node thread is interrupted while waiting, then selfInterrupt() is added to the interrupt.

Now that the exclusive access to shared state is complete, let's take a look at the process of releasing shared state.

3.2 Release of Shared State

release(int arg)

The exclusive release of the top-level entry (int arg) method of releasing shared resources thoroughly releases the shared state (state = 0) and wakes up its successor nodes to obtain the shared state.

Method source code

 public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                // Wake up the successor node of the head node.
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

Source code analysis
tryRelease(arg)

protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

The semantics of tryRelease(int arg) and tryAcquire(arg) are basically the same, leaving subclasses to implement.

unparkSuccessor(h)

private void unparkSuccessor(Node node) {
        // Get the waiting state of the current node
        int ws = node.waitStatus;
        
        if (ws < 0)
            // If the node state is less than 0 (), set its state to 0
            compareAndSetWaitStatus(node, ws, 0);
         // Get its next wake-up node
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            // If the next node is null, or the waiting state is greater than 0 (cancelled state), continue looking down
            //Till the node whose waiting state is less than or equal to 0
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            // Wake up the node to wait for the thread
            LockSupport.unpark(s.thread);
    }

Summary

  • First, the implementer's tryRelease() is called, and if it fails, false is returned.
  • Success finds the next valid node and wakes it up.

The source code for obtaining synchronization and releasing synchronization state in this exclusive mode has been analyzed. Is there any Money? Don't be afraid to end up with a flowchart to help you understand.

Combined with the above source code analysis, we should have a better understanding of AQS exclusive access and release of shared state source code.

Four, summary

This paper analyses the acquisition and release process of exclusive synchronization state, sums up appropriately: when acquiring synchronization state, synchronizer maintains a synchronization queue, threads that fail to acquire state will join the queue and spin in the queue. The condition of queuing (or stopping spinning) is that the precursor node is the head node and the synchronization state is obtained successfully. When the synchronization state is released, the synchronizer calls the tryRelease(int arg) method to release the synchronization state, and then wakes up the successor node of the head node.

Posted by WindChill on Tue, 23 Apr 2019 22:54:35 -0700