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.