Abstract queued synchronizer (AQS) learn about the principle of JUC framework

Time:2021-4-19

brief introduction

AQS (Abstract queued synchronizer) is a basic component in concurrent development. It mainly implements synchronization state management, thread queue management, thread waiting, thread wake-up and other underlying operations. Many concurrency classes in JDK depend on AQS. Reentrantlock, semaphore, countdownlatch.

Lock is simple and practical

  • Before introducing the principle, let’s take a brief look at the use of lock.
public static void main(String[] args) {
    Integer index = 0;
    ReentrantLock lock = new ReentrantLock();
    List threadList = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
        int finalI = i;
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(new Random().nextInt(100));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.lock();
                System.out.println(finalI);
                lock.unlock();
            }
        });
        threadList.add(thread);
    }
    for (Thread thread : threadList) {
        thread.start();
    }
}
  • It is the use of lock and unlock. To ensure the orderly execution of the intermediate business. It doesn’t guarantee that the output numbers are in order, but it can guarantee that the number of outputs is 100, because here we understand that they will enter the queue. But the order of entry is uncertain. Now let’s look at the relationship between lock and unlock and our protagonist AQS today.

Main frame

AQS provides a framework for blocking locks and synchronizers that rely on FIFO (first in first out) waiting queues. The class is an abstract class. The exposed methods are mainly used to judge the operation state and category. We do not need to consider blocking methods in these methods, because the way to call these methods in AQS will deal with blocking problems.

method describe
boolean tryAcquire(int args) Attempt to acquire exclusive lock
boolean tryRelease(int args) Attempt to release exclusive lock
int tryAcquireShared(int args) Try to get shared lock
boolean tryReleaseShared(int args) Attempt to release shared lock
boolean isHeldExclusively() Does the current thread acquire an exclusive lock

Other methods have AQS class implementation. Methods implemented in AQS call the above abstract methods. Normal subclasses are rendered as inner classes. This kind of advantage can achieve the closed synchronization property. Introduction of AQS internal implementation

method describe
void acquire(int args) Obtain the exclusive lock and call the tryacquire method internally,
void acquireInterruptibly(int args) Response to interrupt version of acquire
boolean tryAcquireNanos(int args , long nanos) Response to interrupt + timeout version of acquire
void acquireShared(int args) Obtain the shared lock and call the tryacquireshared method internally
void acquireSharedInterruptibly(int args) Get shared lock in response to interrupt version
boolean tryAcquireSharedNonos(int args,long nanos) Response to interrupt + timeout to acquire shared lock
boolean release(int args) Release exclusive lock
boolean releaseShared(int args) Release shared lock
Collection getQueuedThreads() Gets the collection of threads on the synchronization queue

Principle analysis

AQS internal is through a two-way linked list to manage the lock (commonly known as CLH queue).
When the current program fails to acquire the lock, it will wrap the current thread as an AQS internal class node object, add it to the CLH queue, and suspend the current thread. When a thread releases its own lock, AQS will try to wake up the thread directly following the head in the CLH queue. AQS We can make different needs according to his status. I’ll talk about it later. Next, we have reentrantlock to illustrate the principle of AQS.

  • Marked above is the lock method in reentrantlock. This method means to lock. Anyone who knows lock knows that this method will block all the time, and it will be executed only when the lock is successful. and ReentrantLock.lock Method to lock the actual sync object. There are two kinds of sync in reentrantlock: Fair lock and unfair lock.

  • In AQS, the default is non fair lock, that is, random wake-up thread.


  • Through the above inheritance relationship, we found our protagonist today – Abstract queue synchronizer.

  • Nonfairsync implements two methods: lock and tryacquire. The lock is realized by the status bit- Not locked; 1- It’s locked If the lock is successful, the state will be set to 1 and the thread in exclusive mode will be the current thread. Otherwise, call acquire to try to acquire the lock.

Exclusive lock

AQS data structure

  • AQS is mainly the management of status bits. Let’s look at the included properties
Class AbstractQueuedSynchronizer{
    /*The head node in the queue has no practical significance. The successor node of the head is the first node in the queue*/
    private transient volatile Node head;
    /*Tail node in queue*/
    private transient volatile Node tail;
    /*The state of the queue, lock and unlock can be extended to different states. AQS is actually the management of this field. State management through get set compare method in subclass*/
    private volatile int state;
}

CLH data structure

  • As we learned above, threads will be packaged as node objects and added to the bidirectional linked list (CLH). Let’s take a look at the structure of node
static final class Node {
    /*Marking of shared mode*/
    static final Node SHARED = new Node();
    /*Mark of exclusive mode*/
    static final Node EXCLUSIVE = null;
    /*Queue waiting status - cancel*/
    static final int CANCELLED =  1;
    /*Queue waiting status - wake up*/
    static final int SIGNAL    = -1;
    /*Queue wait status - conditional wait*/
    static final int CONDITION = -2;
    /*Queue waiting status - Broadcast*/
    static final int PROPAGATE = -3;
    /*Queue waiting state, the value range is one of the above waiting states*/
    volatile int waitStatus;
    /*Precursor node*/
    volatile Node prev;
    /*Successor node*/
    volatile Node next;
    /*Thread corresponding to node: binding relationship*/
    volatile Thread thread;
    /*TODO*/
    Node nextWaiter;
    /*Determine whether it is sharing mode*/
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    /*Gets the precursor node of the current node. If there is no precursor node, NullPointerException will be thrown*/
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
    /*It is used to create the head node in the bidirectional linked list. In fact, the head node is a flag and will not be linked with threads. Equivalent to the default head node of a queue. Or to create a shared mode node. Because the nodes in shared mode are parameterless*/
    Node() {
    }
    /*The thread is packaged as a node object and added to the queue. The source code is used to add thread to the queue*/
    Node(Thread thread, Node mode) {
        this.nextWaiter = mode;
        this.thread = thread;
    }
    /*Adding common words to conditional state queue todo*/
    Node(Thread thread, int waitStatus) {
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

Implementation steps of acquire

  • As we learned above, the underlying implementation of lock in lock is implemented by AQS acquire.

  • By looking at the source code, we can probably understand the locking process,

    • First try to get the lock
    • After the lock acquisition failure, the current thread is wrapped as a node object and added to the CLH queue
    • Block the current thread and wait for the queue to wake up
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

addWaiter

/**
 *Construct the node object through the constructor of the node object and add it to the CLH queue
 *This method is mainly two-way list operation. C + + students should be easy to understand
 */
private Node addWaiter(Node mode) {
    /*After the current thread joins the queue, it has no successor node and has exclusive access mode
    *So the node added here is not passed in in the previous node Node.EXCLUSIVE This means that
    *It is locked in exclusive mode to join the queue
    */
    Node node = new Node(Thread.currentThread(), mode);
    /*Get the last node in the queue; Here is a quick insert test.
    *The default queue is already accumulating node nodes. At this time, the node is directly appended to the tail.
    *In fact, the logic here is the same as that of the enq () method. But there will be a waiting queue in enq
    *Only when it's normal
    */
    Node pred = tail;
    if (pred != null) {
        /*When the queue has generated thread waiting, the precursor node of the current node will only be tail
        *Replication node for
        */
        node.prev = pred;
        /*Based on CAS (internal unsafe Implementation), the tail is set as node*/
        if (compareAndSetTail(pred, node)) {
            /*The successor node of the original tail node is naturally the node node*/
            pred.next = node;
            /*At this point, the node has been added to the CLH queue*/
            return node;
        }
    }
    /*The logic is the same as above*/
    enq(node);
    return node;
}

acquireQueued

  • The node passed here is the node we just added to the end of the team. Why not use the tail node directly? We observed the tail carefully
private transient volatile Node tail;
  • We know thatvolatileIs memory visible. What is memory visibility. Our attribute variables are stored in memory. Every time a thread starts to access this class, it will copy the property values in memory to its own thread. Therefore, in the case of multithreading, modifying this property will cause problems, because thread a modifies the value, but thread B cannot perceive and interact with the original value. This is a typical multithreading problem. andvolatileWe have achieved the goal of thread awareness. When thread a modifies the tail, thread B immediately senses it. But this can not completely solve the problem of multi concurrency. Here we briefly introduce this keyword
  • After a brief description of the high concurrency scenario above, we can’t use tail directly here. Because at this time, the tail may not belong to our tail. It’s very wise to pass node directly here. And it’s final. It is more guaranteed that we added the node to the end of the team in the previous step
/**
 *Try to get lock again, not sensitive to interrupt.
 */
final boolean acquireQueued(final Node node, int arg) {
    /*Failure flag bit*/
    boolean failed = true;
    try {
        /*Thread interrupted flag bit*/
        boolean interrupted = false;
        /**/
        for (;;) {
            /*Get the precursor node of the node that you want to wrap*/
            final Node p = node.predecessor();
            /*If the precursor node is a head node, it means that the current node can try at the head of the team
            *Get the next lock. Why do you try to get it here? It's possible to get the next lock at this time
            *It's also occupied by other threads. Trying to get here is just trying to get a chance
            */
            if (p == head && tryAcquire(arg)) {
                /*Successful access to the lock, that we try the mentality of success.
                *It's the same in life. You have to try. If you succeed. Look at the source code
                *Can learn the truth of life. Key points
                */
                /*At this time, the lock has been occupied by the current thread in tryacquire.
                *We don't need to worry that other threads will preempt here. At this time, we will
                *You need to kick the current thread out of the queue and directly set the current thread to
                *Head node. Sethead The method is also very simple
                *The dot is set to null because the head is the first and should not be placed before the first
                *There are nodes, and then the thread is destroyed
                */
                setHead(node);
                /*The P node is an old head node, which is no longer needed at this time.
                *Here, the operation of JDK is to set next to null, so that the P node
                *It becomes an unreachable state, and the next destiny is to wait for the GC to be released.
                *The reason why we don't set p to null here is that we have p = null, 
                *It just points P to null, but the node of the original head is null
                *If the address is still pointed to by node, GC cannot recover it. Understand*/
                p.next = null; // help GC
                /*We've got it here. And it's locked. So here it is
                *We can't cancel the acquisition, and we've removed the node, too
                *There is no need to cancel the get operation. So in finally
                *There's no need to do it*/
                failed = false;
                /*Returns whether the thread is interrupted*/
                return interrupted;
            }
            /*If the node corresponding to the current thread is not the successor node of the head, or
            *We didn't get the lock. At this time, we began to block the thread*/
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            /*Cancel the node corresponding to the current thread to queue in the queue. Here you can
            *It is understood as abstention operation. If you cancel, you will traverse the previous node by the way
            *Those who abstain will be operated at the same time
            */
            cancelAcquire(node);
    }
}

shouldParkAfterFailedAcquire

/**
 *In the case of failure to obtain the lock, judge whether the thread needs to be blocked and agree to modify the thread
 *Status in the queue. If the precursor node is in the signal state, then the node enters
 *Ready. The precursor node canel status needs to be removed. If it's condition or
 *Progate state. In reentrantlock, we don't consider these two cases for the time being,
 *So it's forced to switch to the signal state here
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    /*Gets the state of the precursor node*/
    int ws = pred.waitStatus;
    /*If the precursor node is waiting for notification, the current node needs to wait for the precursor
    *The node is awakened, so it needs to be blocked here
    */
    if (ws == Node.SIGNAL)
        return true;
    /*If the precursor node > 0, it is cancled*/
    if (ws > 0) {
        //In fact, the logic here is similar to that of cancelacquire, and the cancelled nodes need to be removed from the queue
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*In the remaining cases, the node status is corrected to waiting for notification*/
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

parkAndCheckInterrupt

/**
 *Block the current thread and wait for it to wake up
 */
private final boolean parkAndCheckInterrupt() {
    /*This is blocking the thread and waiting LockSupport.unpark Wake up*/
    LockSupport.park(this);
    /*After Park, we need to Thread.interrupted Restore the interrupt state of the next thread,
    *So the next park will take effect. Otherwise, the next park will not take effect
    */
    return Thread.interrupted();
}

cancelAcquire

/**
 *Delete all cancellations before the node (including the current node)
 */
private void cancelAcquire(Node node) {
    if (node == null)
        return;
    /*The weeding operation needs to unbind the relationship between node and thread*/
    node.thread = null;
    /*Get the precursor node of the node*/
    Node pred = node.prev;
    /*Greater than 0 is the cancellation status*/
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
    Node predNext = pred.next;
    /*This is directly set to cancel state to facilitate other threads to cancel,
    *It is also for the convenience of jumping the node
    */
    node.waitStatus = Node.CANCELLED;
    /*If the node is the haul of the tail of the team, then the tail of the team is set as the precursor node of the node*/
    if (node == tail && compareAndSetTail(node, pred)) {
        /*It is the standard requirement of a queue to leave the successor node of the pred node at the end of the queue empty*/
        compareAndSetNext(pred, predNext, null);
    } else {
        //If it is a non tail node
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
                (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            /*If the pred node state is a valid node and not a head, it will be the successor of PRED
            *The node points to the node's successor. Here and C + + pointer point is a truth*/
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                /*The successor node of the node is a valid node and is not in cancel state*/
                compareAndSetNext(pred, predNext, next);
        } else {
            /*
            *This is to release the above-mentioned blocking. inside
            *Actually LockSupport.unpark For release.
            *At this time, we know from the above if that this time appears in the following scenarios
            * 1、pred==head
            *2. PRED is the cancel status
            * 3、 pred.thread==null  It is not a valid node
            *All of the above situations indicate that PRED is not a node that can wake up
            *It is not a standard node. At this time, in order to keep the queue active,
            *We need to wake up the successor node, which is actually the successor node of the node.
            */
            unparkSuccessor(node);
        }
        node.next = node; // help GC
    }
}
  • In the above code, when the code executes tounparkSuccessor(node)This block will wake up the node. However, our canelacquire method is to cancel the node in the cancelled state before the node is cancelled. That would be against our function. The naming method is to eliminate the canel node. Now do wake up the node. Here we are up thereshouldParkAfterFailedAcquireIn the method, when the state is > 0, these nodes are automatically rejected. In this way, the function of canelacquire method is realized. So we don’t have to struggle.
    PS: the source code is the source code after all. What we consider is very comprehensive.
if (ws > 0) {
    //In fact, the logic here is similar to that of cancelacquire, and the cancelled nodes need to be removed from the queue
    do {
        node.prev = pred = pred.prev;
    } while (pred.waitStatus > 0);
    pred.next = node;
}

unparkSuccessor

/**
 *Wake up node
 */
private void unparkSuccessor(Node node) {
    /*Gets the status of the current node*/
    int ws = node.waitStatus;
    /*Judge the state*/
    if (ws < 0)
        /*If it is less than 0, the forced correction is 0*/
        compareAndSetWaitStatus(node, ws, 0);
    /*Gets the successor node of the current node*/
    Node s = node.next;
    /*Judgment*/
    if (s == null || s.waitStatus > 0) {
        /*If the successor node is a valid node and the state is > 0, it is cancelled state,
        *Then the node is removed from CLH and connected by fault*/
        s = null;
        /*This is the same as the forward node that removes the cancellation state, except that this is backward
         *As for why it is from the back to the front, it is to avoid the node inconsistency caused by high concurrency
         *It's very interesting. Because if you start from node, it is very likely that other people will be killed later
         *The thread has been modified. Because the node is added later. So from the back to the front, it can ensure that the data is consistent. However, this will cause the nodes added by other threads to be inaccessible. This is more important than data consistency. It doesn't matter if we can't get the lock this time. When we get the lock, JDK uses the for loop. Will keep checking whether the nodes in the queue can be awakened. Here we understand it as a timer. So it doesn't matter if you can't get the node at one time. It's going to wake up one time. 
         */
        for (Node t = tail; t != null && t != node; t = t.prev)
            /*The state of the head node should be 0, so the last s here is the head. So the next node that releases * is the successor node of the head. */
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        /*This corresponds to the one in parkandcheckinterrupt
        *  LockSupport.lock (this) method. unpark
        *The parkandcheckinterrupt method then executes to Thread.interrupted
        *And return, this time return true*/
        LockSupport.unpark(s.thread);
}

acquire

  • Here, we read the steps of acquire one by one according to the method dimension. The first step is to acquire the lock. If the acquisition fails, the thread will join the queue. At this time, the thread will be blocked. In the process of joining the queue, the invalid node will be removed (such as cancel status or null parameter). Ensure that all nodes in the queue are effective and active nodes. This process ensures that the queue is running. If joining the queue is successful, the next step is to suspend the thread by itselfThread.currentThread().interrupt();In fact, this step means that the thread is no longer needed. It was cancelled. This thread will be invalidated later.

The following is a schematic diagram from the God of blog Garden

release

  • The logic of obtaining exclusive lock is very complex, which involves the operation of bidirectional linked list. It should be very difficult if you have not touched C + +. In fact, the logic of acquisition involves the logic of release. When we wake up the successor node of the node, it is also an important part of the release logic.


public final boolean release(int arg) {
    /*Will call tryrelease, which is implemented by subclass. We're at reentrantlock
    *Should be a tryrelease implemented by an unfair lock. This method will be said later.
    *Here we need to mention a point: when a thread acquires a lock, its corresponding node is
    *No longer in the queue. So releasing here can be understood as waking up the successor node of the head.
    *This is the same as the following node that wakes up the node above. So you'll see the same thing
    *Method * unparksuccess (H)
    */
    if (tryRelease(arg)) {
        /*Get the first node in CLH queue*/
        Node h = head;
        if (h != null && h.waitStatus != 0)
            /*Wake up the successor node of the head node*/
            unparkSuccessor(h);
        return true;
    }
    return false;
}
  • Here we need to explain why we judge the head node. Because head in AQS is null by default. So what is the creation of head. It is added when we add the lock above, and the head is created when we need to judge the precursor node after joining the queue. At this time, the head has no state set. Then this state is 0 by default. So the above judgment only needs to be empty. But for the sake of strictness, JDK makes a double judgment.
private transient volatile Node head;
  • So we need to judge the head empty.

tryRelease

  • In fact, in the above explanation of acquire steps, we missed ittryAcquireMethods of reading. The purpose is to communicate with otherstryReleaseMethods combined explanation. Because both methods are implemented by subclasses. Together, we can understand design better. In reentrantlock, tryacquire is implemented by nonfairtryacquire with unfair lock
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    /*First, get the state of the exclusive lock*/
    int c = getState();
    if (c == 0) {
        /*C = = 0 indicates that the current exclusive lock is not occupied by any thread. It can be locked at this time*/
        if (compareAndSetState(0, acquires)) {
            /*Set the thread currently owned as the current thread*/
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        /*Because of this judgment, a re entrant lock is implemented, so that a thread can repeat the lock operation. */
        /*C! = 0 indicates that the thread is occupied. If the representation of the current thread is re entered. Then the exclusive lock state will continue to accumulate. The state here is the state of AQS and the waitstaus in node are two different things. Here, accumulation in the release method is decrement. So the comparison is easy for us to understand. Different implementations of status here have different positioning functions*/
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            /*I really don't understand the judgment here. I hope the great God can give me some advice. */
            throw new Error("Maximum lock count exceeded");
        /*CAS set state*/
        setState(nextc);
        return true;
    }
    return false;
}
protected final boolean tryRelease(int releases) {
    /*After reading the incremental operation in tryacquire, we can understand the logic of decrement here*/
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        /*C = = 0 indicates that the thread completely releases the exclusive lock because of the reentrant locking mode. This time can be occupied by other threads*/
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

Shared lock

  • The main application scenario of shared lock is reading. The exclusive lock application scenario is the write scenario. This is inReentrantReadWriteLockClass. Let’s go through the lockReentrantLockRead it again. Now let’s go throughReentrantReadWriteLockLet’s experience the logic of shared lock.

  • The shared lock logic has changed. But the methods involved are mentioned in exclusive lock. Now we’ll talk about methods that we haven’t mentioned. The common way is smart. You should read and understand.


  • sametryAcquireSharedI’m not going to look at it here. Read later with the release method. Let’s go through firstdoAcquireSharedMethod for the entrance to read

Get shared lock

doAcquireShared

/**
 *This method has the same logic as exclusive lock acquire. It's just that all the methods are mentioned inside the method.
 *Addwaiter and exclusive lock is a method
 *The following for loop is the same. If it is the successor node of the head, it will try to obtain the lock and change the head for *. And if the thread is blocked, it will interrupt the thread and other operations. So it's easy to learn shared lock after reading exclusive lock. Although they are different, they are very similar
 */
private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                /*If the precursor node is the head node, it will try to acquire the lock, which may succeed*/
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    /*If it is successful, the node will be eliminated, so that the head node points to the latest node*/
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

setHeadAndPropagate

/**
 *Progagate represents the capacity of the current shared lock
 *Node represents the node corresponding to the current thread
 */
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);
    /*
     *Progagate > = 0 has been ensured outside the method
     *Progagate = 0 indicates that the current shared lock cannot be acquired. So here are the conditions
     *One is progagte > 0
     *1. If propagate > 0, you will check whether the successor nodes in the queue meet the conditions. If yes, you will wake up the successor nodes of the head in the queue through the doneleaseshared method
     *2. Head = = null indicates that AQS has not created a head yet. At this time, the way to start releasing is to start the process of releasing. The internal implementation is a for loop. It is equivalent to listening to the head node
     * 3、 head.waitStatus < 0 indicates that the value is set to
     *  Node.PROGAGATE It's too late. When the lock is released, the state of head will be set
     *When signal is set to 0, it will also be set from 0 to progate. The default state of the head node is also 0,
     *Therefore, if the head state is less than 0, the resource can only be released by another thread
     *Executed the code set to progate. Although propagate = = 0, but only
     *Get that. 0 will be changed in high concurrency scenarios. Now that another thread is released
     *Resources, then it is natural to wake up the queue thread to try to obtain them. Here is the condition
     *We'll sort out this place again after the whole logic
     */
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

doReleaseShared

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            /*Get head*/
            int ws = h.waitStatus;
            /*
             *The state of the head node is 0 by default, so the first time in the queue it should enter
             *In the following if, set the head node to propagation state; set it to propagation state
             *The purpose of the state is to facilitate the corresponding of our method
             *Judge h.waitstatus < 0. This will wake up the head node
             *It's the successor node of. This time may fail, but sharing is to let him
             *We get as much as we can. So set the propagation state here. It's possible
             *It will be propagated through the shouldparkafterfailedacquire method
             *The state correction is signal, that is, it will be corrected later. this
             *It needs to be compared with shouldparkafterfailedacquire,
             *Shouldparkafterfailedacquire is a sign state pair encountered
             *The subsequent node blocks, and in this case, it releases when it encounters the signal state
             */
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                /*Same as exclusive lock*/
                unparkSuccessor(h);
            }
            /*Here is to set the propagation state, corresponding to the setheadandpropagate method*/
            else if (ws == 0 &&
                        !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

Release shared lock

  • Tryrelease shared is implemented by subclasses in the same way. Later, let’s look at the implementation logic of these two methods through the reentrantreadwritelock class. Finally, the release logic of AQS is put on the doneleaseshared method.

doReleaseShared

  • When reading the above, after setting the head node, the following nodes will be checked to determine whether to wake up or not, which is called do release shared. So this method doesn’t need to be mentioned here.

tryAcquireShared

/**
 *If there is no conflict between the read lock and the write lock, the write lock will be acquired until it is successful,
 *Get failed, return - 1
 *Get success return 1 
 */
protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    /*Exclusive count is a sum operation of C and exclusive lock capacity. Shared capacity 2 ^ 16-1  
     *So as long as C! = 0, exclusivecount (c) is! = 0
     *Is the current thread. This is also the certificate of the re entrant lock
     */
    /*
     *The read lock and the write lock are mutually exclusive, so if other threads have acquired the write lock, then
     *You can't get the lock if you read it.
     */
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    /*Sharedcount is to obtain the capacity of shared lock*/
    int r = sharedCount(c);
    /*Readershouldblock is to determine whether the node needs to be blocked, as long as there is
     *If the effective node is a shared node, it will not block; Read lock write lock is a 32-bit, high bit write lock
     *Lock low read lock, shared_ Unit is the lower 16 bits, so here is to increase the number of read locks*/
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            /*For the first time*/
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            /*The thread that reads for the first time reads repeatedly, and the total number of times the thread reads*/
            firstReaderHoldCount++;
        } else {
            /*In fact, a ThreadLocal manages the number of reads. It works the same as the first reader above*/
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    /*In high concurrency scenarios, CAS stack times may not be successful. At this time, * fulltryacquireshared needs to acquire read lock again. This method logic is similar to the above
    *It's the same. So why is it called full, because it uses loops to ensure that there are residual conditions
    *Next, get the read lock. It won't be unavailable because of CAS problems*/
    return fullTryAcquireShared(current);
}

tryReleaseShared

/**
 *Here, as long as there is a read lock, it will return false,
 *If false is returned, AQS release cannot release the queue. This is the case
 *Because the queue itself is active. The locks will be released in sequence. And the release of the read lock
 *In fact, it was released in tryreeseshared. Read lock is actually counting.
 *This is explained in detail in the chapter reentrantreadwritelock
 */
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {
        //The current thread is the first to acquire a read lock. The number of times that we will add here is decreasing.
        //When the current thread is all released, it contacts the current thread's occupancy
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        //Here, the thread that is not the first to acquire the read lock is released. Release of display times
        //The thread is discarded when it is completely released
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    //This corresponds to fulltryacquireshared above. Cycle until the release is successful
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            // Releasing the read lock has no effect on readers,
            // but it may allow waiting writers to proceed if
            // both read and write locks are now free.
            return nextc == 0;
    }
}

summary

AQS is an underlying principle of concurrency class in JDK. Many JDK concurrency classes are based on this implementation. AQS It’s actually a framework. Just a few words

  • AQS is a base class for concurrency
  • FIFO queues are maintained internally
  • It has two modes: exclusive mode (write lock) and shared mode (read lock)

The internal state is the state of the lock. Different implementations can have different definitions.

Reentrantlock: state of pure lock + 1, – 1
Semaphore: number of locks
Countdownlatch: counter, a flag bit