J.U.C|AQS Shared Source Analysis

Posted by milind24 on Tue, 14 May 2019 21:55:04 +0200

1. Write before

In the last section, we talked about the exclusive source code, see J.U.C|AQS Exclusive Source Analysis

In this chapter, we continue to navigate the world of AQS source code to understand the acquisition and release of shared synchronization.

2. What is Shared

The only difference between shared and exclusive is that multiple threads can get synchronized at the same time.

Let's take a read-write lock as an example. When a thread reads a resource file, the write operations to the file are blocked at that time, while the read operations of other threads can occur simultaneously.
When a write operation requires an exclusive operation on a resource and a read operation can be shared, what would two different operations operate on the same resource?Look at the picture below


Shared access to resources, other shares are allowed, and exclusive access is blocked.


Other access is blocked when the resource is accessed exclusively.

By reading and writing locks to review the concepts of exclusivity and sharing, we have already talked about exclusivity in the previous section, and in this chapter we mainly talk about sharing.

Main Explanatory Methods

  • protected int tryAcquireShared(int arg); Shared fetches synchronization status with return value >= 0 indicating success or failure.
  • protected boolean tryReleaseShared(int arg): Shared release synchronization state.

3. Analysis of Core Methods

    • *

3.1 Getting Synchronization Status

public final void acquireShared(int arg)

Shared Get Synchronization Top Entry. If the current thread does not get synchronization, it will join the synchronization queue and wait. The only difference from exclusive is that multiple threads can get synchronization at the same time.

Method Source

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

Method Function Analysis

  • tryAcquireShared(arg): Gets the synchronization status, and returns a value greater than or equal to 0 indicating success or failure.
  • doAcquireShared(arg): Shared access to the shared state, including building nodes, joining queues waiting, waking nodes, and so on.

Source Code Analysis

acquireShared and doAcquireShared methods for Synchronizers

//Entry Requesting Shared Lock
public final void acquireShared(int arg) {
        // Get resources only when state!= 0 and tryAcquireShared (arg) < 0
        if (tryAcquireShared(arg) < 0)
            // Acquire locks
            doAcquireShared(arg);
    }
// Acquire locks in shared uninterruptible mode
private void doAcquireShared(int arg) {
        // Build the current thread-one sharing into a node and add it to the end of the synchronization queue.Here the addWaiter(Node.SHARED) operation is basically the same as exclusive.
        final Node node = addWaiter(Node.SHARED);
        // Mark success
        boolean failed = true;
        try {
            // Is the wait process marked as interrupted
            boolean interrupted = false;
            //spin
            for (;;) {
                // Get the precursor node of the current node
                final Node p = node.predecessor();
                // Determine if the precursor node is the head node, that is, whether you are the second-most node?
                if (p == head) {
                    // If you are the second node, try to acquire a resource lock and return to three states
                    // State < 0: Failed to get resource
                    // state = 0: Indicates that the current thread is getting resources without propagating to subsequent nodes.
                    // State > 0: Indicates that after the current thread has acquired a resource lock, there are additional resources that need to be propagated to subsequent nodes to acquire resources. 
                    int r = tryAcquireShared(arg);
                    // Success in getting resources
                    if (r >= 0) {
                        // Logical operations on subsequent nodes after the current node thread has successfully acquired resources
                        setHeadAndPropagate(node, r);
                        // setHeadAndPropagate(node, r) has node.prev = null, p.next = null here; waiting for GC to garbage collect.
                        p.next = null; // help GC
                        // If the waiting process is interrupted, the replenishment will be interrupted.
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                // Judges the state, finds the safe point, enters the waiting state, waits for unpark() or interrupt()
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

In the acquireShared(int arg) method, the synchronizer calls the tryAcquireShared(arg) method to get the synchronization state, and returns the synchronization state in two ways.

When the synchronization state is greater than or equal to 0: indicates that the synchronization state can be obtained and the spin can be exited. In the doAcquireShared(int arg) method, you can see that the condition for a node to get resources to exit the spin is greater than or equal to 0

Less than 0 joins the synchronization queue waiting to wake up.

addWaiter and enq methods

// Create a node and add it to the end of the synchronization queue.
 private Node addWaiter(Node mode) {
        // Building Node Nodes for Threads Shared
        Node node = new Node(Thread.currentThread(), mode);
        // Try to quickly join the end of the queue
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            // CAS guarantees atomic operations, adding node nodes to the end of the queue
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // Quick join failed, take enq(node) method
        enq(node);
        return node;
}
//Spin the node to the end of the queue
private Node enq(final Node node) {
        // spin
        for (;;) {
            // Get Tail Node
            Node t = tail;
            // If tail node is empty, synchronization queue has not been initialized and must be initialized first
            if (t == null) { // Must initialize
                // CAS guarantees atomic operations, creates a new empty node and sets it as the head node
                if (compareAndSetHead(new Node()))
                    // Setup succeeded and tail pointed to the node as well
                    tail = head;
            } else {
                // Add node node to end of queue
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

These two methods are essentially the same as exclusive ones, which are noted in the notes and are not explained much here.

setHeadAndPropagate method for operation on subsequent nodes after successful resource acquisition

private void setHeadAndPropagate(Node node, int propagate) {
        // Record the old head node for checking
        Node h = head; // Record old head for check below
        // Set node as head node
        setHead(node);
        // This means: if the resource is sufficient (propagate > 0) or if the old header node is empty (h == null) or the waitStatus of the old node is SIGNAL (-1) or PROPAGATE (-3) (h.waitStatus < 0)
        // Either the current head node is not empty or the waitStatus is SIGNAL (-1) or PROPAGATE (-3), at which point you need to continue waking up subsequent nodes to try to obtain resources.
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            // The successor node of the current node
            Node s = node.next;
            //If the back node is empty or belongs to a shared node
            if (s == null || s.isShared())
                // Continue trying to get resources
                doReleaseShared();
        }
    }

First, set the current node to the head node setHead(node), and then, depending on the condition, see if the successor node continues to wake up.

Failed to get resources, blocked waiting for unpark

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // Get the wait state of the precursor node
        int ws = pred.waitStatus;
        // If the wait state is already SIGNAL (indicates that the successor node of the current node is in the wait state, or wakes up the successor node if the current node releases the synchronization state or is interrupted)
        if (ws == Node.SIGNAL)
            // Return directly to indicate that you can rest safely
            return true;
        // If the state of the precursor node WS > 0 (indicates that the node has been cancelled or interrupted, i.e. invalid and needs to be cancelled from the synchronization queue)
        if (ws > 0) {
            // Loop forward and know to find a valid security point (a node with a wait state <= 0, behind it)
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            // Note that after this wave of operations, all award-winning canceled nodes become GC recyclable waste chains.
            pred.next = node;
        } else {
            //If the predecessor is working, set the state of the predecessor to SIGNAL and tell it to notify itself once it has acquired the resources.It may fail, someone may have just released it!
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

 private final boolean parkAndCheckInterrupt() {
        // Call the park method to get the current node's thread into waiting
        LockSupport.park(this);    
        //Return thread interrupt state
        return Thread.interrupted();
    }

These two methods are basically the same as exclusive.

Next, look at the more complex doReleaseShared

private void doReleaseShared() {
        //Notice that the head node here is already the newly set head node above, and you can see from this that if propagate=0,
        //Not going into the doReleaseShared method, then sharing becomes exclusive
        for (;;) { // Dead loop to prevent adding a new node when performing this operation: exit condition h == head
            Node h = head;
            // The precondition is that the current head node is not empty and not the tail node
            if (h != null && h != tail) {
                // Waiting state of current header node
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    // If the current node's state is SIGNAL, CAS is used to set its state to 0 (that is, initial state)
                    //This is not set directly to Node.PROPAGATE because in unparkSuccessor(h), if ws < 0 is set to 0, ws is set to 0 first, then PROPAGATE
                    //You need to control concurrency here, because the entries have setHeadAndPropagate and release, avoid unpark twice
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases setup failed, recycle
                    // Wake Up Subsequent Nodes
                    unparkSuccessor(h);
                }
                // If the wait state is not 0, CAS is used to set its state to PROPAGATE to ensure that successor nodes can continue to be notified when resources are released.
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed if a change occurs during the head, it needs to be made from scratch
                break;
        }
    }
private void unparkSuccessor(Node node) {
        
        int ws = node.waitStatus;
        // Again, determine the state of the current header node, if less than 0 it will be set to 0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        //Get succeeding nodes
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            //Discard directly if the successor node is empty or the wait state is greater than 0.
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                // The loop moves forward from the tail to find the next node with a wait state of no more than 0
                if (t.waitStatus <= 0)
                    s = t;
        }
        // The thread that wakes up the node
        if (s != null)
            LockSupport.unpark(s.thread);
    }

3.2 Shared State Release
The last step is to release resources.

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

4. Summary

When synchronization status is acquired, the synchronizer maintains a synchronization queue, threads that fail to acquire status join the queue and spin, and the precursor node is the head node when the conditions of the queue (or stop spinning) are listed and the synchronization status is successfully acquired.When the synchronization state is released, the Release method is called to release the synchronization state and wake up the successor nodes of the header node.

Shared mode determines if the current resource is redundant after the wake-up successor node acquires the resource, and if so continues to wake the next node.

Topics: Java less REST