Location>code7788 >text

Detailed explanation of AQS II: ReentrantLock fair lock principle

Popularity:139 ℃/2024-12-17 22:52:19

ReentrantLock as our most frequently used explicit lock, it is the classic implementation of AQS, this post will be ReentrantLock fair lock as an example to explain the implementation of AQS.

I. ReentrantLock

In a previous post, theThread Synchronization Mechanisms I: Internal and Explicit LocksThe simple use of the explicit lock ReentrantLock has already been mentioned in the

private final Lock lock=new ReentrantLock(); // create an instance of the Lock interface
......

(); // Request the lock
try{
  // Access to shared data here, i.e., this area is critical area code
  ......
}finally{
  // Always release the lock in the finally block to avoid lock leaks.
  (); // release the lock
}

The next step is to dive into the ReentrantLock source code to see its specific implementation.

1, ReentrantLock in the design pattern

The ReentrantLock implementation uses the template design pattern, for more information on the template design pattern, see theDesign Patterns (XV): Template Method Pattern (Template Method Pattern)". The template design pattern involves having an abstract class that implements some of the logic in the form of concrete methods, and then declares some abstract methods to force the subclasses to implement the remaining logic, which is theAbstractQueuedSynchronizerThat is.AQSThe class diagram of ReentrantLock is shown below. Look at the class diagram for ReentrantLock below

AbstractQueuedSynchronizer类图

ReentrantLock internally maintains two types of locks: FairSync and NonfairSync, and ReentrantLock confirms whether the current lock instance is a fair lock or a nonfair lock in the constructor method:

/**
* The default is a non-fair lock
*/
public ReentrantLock() {
    sync = new NonfairSync();
}

/**
* Determine whether a lock is fair or non-fair by passing a parameter to the constructor.
*/
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

The sync in the code is the synchronizer in the ReentrantLock, and the ReentrantLock's locking and unlocking calls are all related to the synchronizer's methods:

/**
* lock
*/
public void lock() {
    ();
}

/**
* release
*/
public void unlock() {
    (1);
}

You can see that ReentrantLock follows the principle of separating the changes from the invariants, encapsulating the changes of the fair and non-fair locks in FairSync and NonfairSync respectively, and the invariant parts are all in AQS; and ReentrantLock itself is oriented towards abstract programming, and its locking and unlocking methods are delegated to the abstract synchronizer Syn, regardless of whether the fair or non-fair locks are used internally, it does not need to change the calling code, which is a typical application of the template method pattern.

2、The core of ReentrantLock: AQS

AQS is ReentrantLock "template method pattern" in the "template", is ReentrantLock explicit lock implementation of the core, if you want to understand ReentrantLock, AQS is a must. AQS is an abbreviation for AbstractQueuedSynchronizer class, which has more than two thousand three hundred lines of code with comments, and can be said to be a very complex class, so how do you go about learning the class? Haphazardly look at the source code is certainly not desirable, the breakthrough lies in the ReentrantLock class lock and release the lock method.

For the explicit lock ReentrantLock, the core method of two: lock and unlock, along with these two methods to find the call chain, but also figure out the principle of the role of AQS. Next, let's take a look at the code structure of AQS.

II. Overview of the AQS code structure

The code implementation of AQS is very long, so here are the more important parts to facilitate source tracking afterwards.

1、Status flag bit

/**
 * The synchronization state.
 */
private volatile int state;

The state variable is a member variable of AQS, which is used to mark the current state of the lock. In ReentrantLock, the state field is initialized to 0, a thread will set it to 1 after grabbing the lock, and reset it to 0 after releasing the lock; when the same thread acquires the lock repeatedly, the field will be accumulated, and when releasing the lock, the field will be decremented until it becomes 0, which is the concept of reentrant.

2、Queue node class

ReentrantLock is an exclusive lock, the thread that fails to grab the lock has to go to the queue, AQS encapsulates the threads into a node one by one, linking the nodes through pointers.

static final class Node {
        /** Node type: indicates that the node is a shared lock node */
        static final Node SHARED = new Node();
        /** Node type: indicates that the node is an exclusive lock node */
        static final Node EXCLUSIVE = null;

        /** Node wait status value 1: the node is canceled */
        static final int CANCELLED = 1;
        /** Node wait status value -1: indicates that the next waiting node needs to be awakened */
        static final int SIGNAL = -1;
        /** Node wait status value -2: indicates that the current node is waiting in the condition queue */
        static final int CONDITION = -2;
        /** Node waiting status value -3: identifies that the next acquireShared operation on a shared lock requires unconditional propagation */
        static final int PROPAGATE = -3; /** Node waiting status value -3.

        /**
         * The wait status value, which can only be one of the following wait status values:
         * SIGNAL: Indicates that the successor node to this node is blocking, so the current node releases the lock
         * The successor node needs to be woken up later. The successor node will try to acquire the lock after it is awakened, * and if it fails, it will enter the lock again.
         * If it fails, it will enter blocking state again and repeat the process.
         * CANCELLED: Canceled state, due to timeout or interruption, the node may be canceled waiting to acquire the lock.
         * Once a node enters this state, it cannot be changed to another state.
         * CONDITION: Indicates that the node is in the conditional wait queue. Note that this state can only be used for
         * CONDITION: Indicates that the node is in a conditional wait queue.
         * PROPAGATE: Indicates that the next acquireShared operation on a shared lock needs to be propagated unconditionally.
         * 0: None of the above
         *
         * There is some significance to the ordering of these values for the wait states, and you'll notice that only the CANCELLED state is greater than 0.
         * A non-negative value means that its subsequent nodes do not need to be woken up. So for the most part, there is no need to care about whether these
         * state values are greater than 0 or less than 0. When you need to wake up subsequent nodes, a simple judgment like greater than 0
         * can simplify the code.
         *
         * In a general synchronized queue, the initial value of the wait state is 0; in a conditional wait queue, the initial value is CONDITION.
         * The method of modifying the change value is generally to use CAS modification to avoid thread security issues.
         */
        volatile int waitStatus.

        /**
         * Pointer to the predecessor node, which can facilitate the current node to query the waiting status of the predecessor node. When entering the wait queue it will
         * be assigned a value, and will be set to null when exiting the queue (for GC purposes). For a canceled predecessor node, the current
         * node will keep looking forward until it finds a node in a non-canceled state, and then update its own predecessor node. Don't.
         * worry about finding a node in a non-canceled state; the head node must not be a node in a canceled state (the head node can only acquire a lock before it
         * can become a head node, so it can't be in a canceled state), so that the query to finally find the head node will also satisfy the condition of updating the
         * condition of the predecessor node.
         *
         */
        volatile Node prev;

        /**
         * A pointer to the succeeding node, which can be used to wake up the succeeding node to acquire the lock after the current node releases the lock.
         */
        volatile Node next;

        /** * The thread that grabbed the lock.
         * The thread to grab the lock.
         */
        volatile Thread thread.

        /**
         * The next waiter under the conditional wait queue.
         */
        Node nextWaiter;
}

The queue node class has a lot of constants: node type and node state, all put together, see the code comments.

There are also five member variables: waitStatus, prev, next, thread, nextWaiter, where waitStatus, prev, next, thread are all modified with the volatile keyword to ensure visibility.

III. Principles of Fair Lock Lock Preemption

ReentrantLock's fair lock FairSync is a bit simpler than non-fair locks, so here's a look at the process of AQS lock preemption using the lock preemption of fair locks as an entry point.

1. FairSync's lock method

static final class FairSync extends Sync {
        final void lock() {
            //invocationsAQSThe template approachacquire
            acquire(1);
        }
    ...Omit other codes...
}

FairSync's locking code is very simple, it directly calls the AQS template method acquire method and passes a parameter 1, other than that there is no code.

2. AQS template method: acquire

/**
 * This method is called only in exclusive mode and ignores the effects of interrupts (no interrupt exception is thrown). The method will first
 * try to call the tryAcquire method to acquire the lock, and if it succeeds it returns directly. Otherwise, the current thread is queued into the queue
 * wait, repeating the blocking-woken-up-tryAcquire steps until it succeeds. This method is usually called by a class that implements the Lock interface
 * and is called by a class that implements the Lock interface via the lock method.
 *
 * @param arg This passed parameter has no special meaning, it can mean whatever we want it to mean, and it * will next be passed as an input parameter to ttc.
 * will be passed as an input to the tryAcquire method call.
 */
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&.
        acquireQueued(addWaiter(), arg))
        selfInterrupt();
}

acquire is to obtain, get the meaning of the method here is actually "to obtain the lock" means. You can see that the code of this method is very simple, but contains a lot of code logic, only it will call the method and return value judgment in the same if judgment statement, so the code is less obvious.

Now look at the four methods called in the acquire method:

  • tryAcquire(arg): try to acquire the lock
  • addWaiter(): the current thread is queued into the waiting queue
  • acquireQueued: spin to preempt the lock, repeating the blocking-woken-up-trying-to-acquire-lock steps until it succeeds.
  • selfInterrupt(): if there is an interrupt in the thread during the process of spin grabbing the lock, then execute the thread interrupt operation.

Anyway, this method is the complete code to grab the lock, and its overall logic is shown in the following figure

image-20241212135504605

3. Hook method: tryAcquire

tryAcquire is a hook method in the AQS class, and is the core lock-grabbing method, which by default throws an exception, meaning that it forces subclasses to override the method (which makes you wonder why it wasn't defined as an abstract method?).

/**
 * This method is used to try to acquire an exclusive lock. In this method the current lock state is queried to see if it is allowed to
 * acquiring the lock, and if it does, takes possession of the lock.
 *
 * This method will be called whenever a thread tries to acquire an exclusive lock. If this method returns false.
 * then it is possible that the current thread will be queued into the wait queue (if it is not already in the wait queue) until another thread
 * releases the lock, then that thread will not have a chance to acquire the lock. This method can be used in the () method.
 */
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

Next, look at the method implementation of the fair lock in ReentrantLock

/**
 * This method will only be called in three cases:
 * 1. when acquiring an exclusive lock for the first time.
 * 2. when the wait queue is empty (there are no more waiters)
 * 3. when a thread in the wait queue is woken up and tries to acquire the lock.
 */
protected final boolean tryAcquire(int acquires) {
    final Thread current = ();
    // Get the current lock state of the AQS synchronizer
    int c = getState();
    // If the lock is idle
    if (c == 0) {
        // Even if the lock is idle, a condition must be met before the lock can be taken.
        if (
            //① Determine if there is still a predecessor waiting node, only if there is no predecessor node will the lock be allowed to be acquired.
            !hasQueuedPredecessors() &&
            //CAS sets the lock state to occupied.
            compareAndSetState(0, acquires)) {
            //Set the thread that succeeded in grabbing the lock
            setExclusiveOwnerThread(current); //Set the thread that succeeded in grabbing the lock.
            setExclusiveOwnerThread(current); return true.
        }
    }
    //②The lock is already occupied, and the thread occupying the lock is the same thread as the current thread.
    else if (current == getExclusiveOwnerThread()) {
        //state accumulates
        int nextc = c + acquires; if (nextc <) {/state accumulates
        if (nextc < 0))
            throw new Error("Maximum lock count exceeded");
        // Update the lock state value
        setState(nextc);
        setState(nextc); // Update the lock state value.
    }
    //Otherwise, if the lock is already occupied and the current thread is not the one occupying the lock, it is not allowed to seize the lock.
    return false; }
}

In the source code above, the code at ① and ② is more interesting, let's start with ②:

ReentrantLock in the Reentrant is actually "reentrant" means that this reentrant can be reflected in the same thread in the case of holding the lock can be repeated to obtain the lock, ② is the realization of the "reentry" of the key. The state value in ReentrantLock is 0, which means the lock is idle, and greater than 0, which means the lock is occupied. Every time the thread holding the lock repeatedly acquires the lock, the value will be increased by 1.

Plus the next ①:

!hasQueuedPredecessors() As a judgment condition is the key condition for whether the current thread can acquire the lock, look at its source code

public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    return h != t &&
        ((s = ) == null ||  != ());
}

The function of this code is to determine whether there are any other nodes queued up before the current node, and if there is a queued predecessor node, then return thetrueOtherwise, returnfalse

In this method block, the basis for determining whether there is a waiting node in front of that node is to determine whether the next node in the head node is the current thread. Why is it important to determine whether the second node in the queue is the current thread? This answer will be answered later when we talk about nodes in and out of the queue.

First think about the question, when there is only one thread, will the AQS queue initialize and queue that thread node? The code example is shown below

public class AQSDemo {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock(true);
        ();
        try {
            ("Hello,word");
        } finally {
            ();
        }
    }
}

When the lock method is called, the code goes back to thehasQueuedPredecessorsmethod, since the AQS queue doesn't exist yet, both tail and head are null at this point, then theh!=tThe determination of thehasQueuedPredecessorsReturns false, and there are no other threads competing for the lock, so CAS is bound to succeed, and the lock will eventually be acquired.

protected final boolean tryAcquire(int acquires) {
    final Thread current = ();
    int c = getState();
    if (c == 0) {
        //When there is only one threadhasQueuedPredecessorswill returnfalse
        if (!hasQueuedPredecessors() &&
            //Since there is no competition,here areCASIt's bound to work.
            compareAndSetState(0, acquires)) {
            //Acquire Lock Ownership
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

The conclusion is that if there is only one thread, that thread will acquire the lock and the AQS queue will not be initialized.

The first thread to obtain the lock, the second thread to obtain the lock, it will inevitably produce lock competition, the second thread to execute tryAcquire will return false, that attempts to obtain the lock failed:

protected final boolean tryAcquire(int acquires) {
    
    int c = getState();
    // Since the first thread has already changed the state field to 1, the second thread does not satisfy the state condition when it reaches this point in the execution.
    if (c == 0) {
        // Assuming that due to high concurrency both threads entered the if block at the same time and the !hasQueuedPredecessors condition is satisfied.
        if (!hasQueuedPredecessors() &&)
            // Since this is a CAS operation, atomicity is guaranteed, so only one of the two threads must succeed
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true; }
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires; if (nextc < 0)
        if (nextc < 0))
            throw new Error("Maximum lock count exceeded"); setState(nextc); setState(nextc); setState(nextc)
        setState(nextc);
        setState(nextc); setState(nextc); return true.
    }
    setState(nextc); return true; }
}

I have written in the above code comments have been written to understand, CAS operation state method call to ensure that no matter what the situation, two threads at the same time tryAcquire to acquire the lock only one success, so what happens to the other failed?

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

The answer is that if it fails it executes the addWaiter method, which means it goes into the queue and waits for the lock to be released.

The tryAcquire flowchart is shown below

image-20241217135243014

4, into the queue method: addWaiter

/**
 * Queue into the wait queue after encapsulating the current thread and specifying the pattern into a new Node
 *
 * @param mode mode mode can be of exclusive type: , or shared type:
 * @return The return value is the encapsulated Node node.
 */
private Node addWaiter(Node mode) {
    Node node = new Node((), mode);
    Node pred = tail;
    // If the queue is not empty, try a fast queue entry first.
    if (pred ! = null) {
         = pred; //If the queue is not empty, try a quick entry first.
        // CAS method for queue entry to prevent thread-safety issues
        if (compareAndSetTail(pred, node)) {
             = node; return node; //CAS method into queue to prevent thread-safety issues.
            return node.
        }
    }
    // If the fast queuing method above fails, the enq method performs spin queuing as a fallback method
    enq(node); return node; }
    return node; }
}

To briefly summarize, the addWaiter method will first try a fast queue entry, and if that fails, it will go through the spin queue entry logic: enq(node).

5. Spin entry: enq

/**
 * Insert a node into the queue, you need to initialize the queue first if necessary.
 * @return The predecessor node of the inserted node, which is not really useful, the return value is not used
 */
private Node enq(final Node node) {
    // Spin and retry to enter the queue until it succeeds.
    for (;;) {
        Node t = tail;
        //If tail node is null it means AQS queue is not initialized
        if (t == null) {
            //The queue must be initialized first
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
             = t; // if the queue is not empty, try to initialize the queue.
            // If the queue is not empty, try to CAS queue to the end of the queue
            if (compareAndSetTail(t, node)) {
                 = node; } else { = t
                return t; }
            }
        }
    }
}

What's interesting is this piece of code that initializes the queue

 if (compareAndSetHead(new Node()))
                tail = head;

Initializing the queue doesn't take the current node to be queued and initialize it, but NEW a new Node that doesn't make any sense to be queued as the head node, why is that?

Because the rules of the AQS queue are just that:The head node is the node of the thread that has acquired the lock, and the second and subsequent nodes are the nodes that will be woken up next.In the explanation of tryAcquire, said, only a thread when the thread will not enter the queue, the second thread failed to seize the lock to enter the queue, the results found that the queue is empty, if it occupies the first head node, it means that it is the thread that holds the lock, which violates the design strategy of the AQS, so here to get a false Node to occupy the head node, so as to ensure that the later So here we need to make a fake node occupy the head node, so as to ensure that the wake-up waiting node process and the lock release process will not cause any problem in the future.

So this queue entry thread, needs to execute at least two for loops to get into the queue, the first to perform the AQS queue initialization, and the second if the CAS queue entry succeeds before it succeeds.

Here is the complete flowchart of the addWaiter method:

image-20241217143832775

6, spin preemptive lock: acquireQueued

/**
 * A method for a thread already in the queue to acquire a lock via exclusive mode, which is unaffected by interrupts.
 * This method is also used equally for threads waiting in the conditional wait queue to acquire a lock.
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true.
    try {
        boolean interrupted = false; for (;;) { boolean failed = true; try {
        for (;;) {
            final Node p = ();
            // If the predecessor node is the head node, the current node tries to seize the lock
            if (p == head && tryAcquire(arg)) {
                // set the lock grabbing node to the head node after successful lock grabbing
                setHead(node);
                // Release the head node for GC.
                 = setHead(node); //Release head node for GC.
                setHead(node); // release the head node for GC = null; failed = false; return interrupted; return
                return interrupted; }
            }
            // if shouldParkAfterFailedAcquire(p, node) && if (shouldParkAfterFailedAcquire(p, node) &&)
            if (shouldParkAfterFailedAcquire(p, node) &&.
                parkAndCheckInterrupt())
                interrupted = true; }
        }
    } finally {
        if (failed)
            cancelAcquire(node); }
    }
}

As soon as the node enters the waiting queue, it starts executing the acquireQueued method to spin up the lock. In this method, the lock grabbing condition is explicitly set: the predecessor node is the head node. This also explains why the enq method of the AQS queue initialization must be a meaningless Node instance as the head node.

From the code, you can also see one thing: after the thread releases the lock, the thread does not remove itself from the wait queue, but by waking up the next node, the next node acquires the lock and then removes the node of the last thread that acquired the lock from the wait queue. At this point unknown can know, AQS wait queue is "lazy loading", wait queue initialization is done in the enq when the first node into the queue, node removal is the next node to wake up to obtain the lock after the removal of the node.

The last thing is that if the lock grab condition is not met, or the lock grab fails, it will execute theshouldParkAfterFailedAcquiremethod determines if the thread should be hung to avoid a lot of wasted cpu resources caused by for loops. This is the biggest difference with the native CLH queue lock: the native CLH queue lock will not hang, it will keep on querying the state of the predecessor node in a dead loop until the predecessor node releases the lock, which wastes a lot of CPU resources (for details, please refer to the articleExplaining AQS in detail I: CLH queue locks》)。

7、Hanging up pre-sentence: shouldParkAfterFailedAcquire

The main function of shouldParkAfterFailedAcquire() method is to find the valid predecessor node of the current node (which means that the valid node is not a CANCELLED node) and set the status of the valid predecessor node to SIGNAL, and then return true to represent that the current thread can be blocked immediately.

/**
 * @param pred The predecessor node of the lock-snatching thread node.
 * @param node The node of the thread to be locked.
 * @return true if it should hang; otherwise false
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = ;
    if (ws == )
        /*
         * If the pred node is in SIGNAL state, the successor node is ready to be pending.
         */
        return true;
    if (ws > 0) {
        /*
         * Greater than 0 specifically refers to CANCELLED state 1, indicating that the predecessor node has been canceled, then the current node should continue the search towards the front
         * Non-CANCELLED state node, find and modify the current node's predecessor node pointer to point to the */ node.
         */
        do {
             = pred = ;
        } while ( > 0);;
         = node.
    } else {
        /*
         * Other types of state: 0, PROPAGATE, CONDITION, then modify the state of the pred node type to SIGNAL.
         * After modifying the state of the predecessor node, the current node will not hang immediately, but will execute tryAcquire once more to make sure that
         */ Ensure that the lock cannot be acquired before the node hangs.
         */
        compareAndSetWaitStatus(pred, ws, );

    return false; }
}

The shouldParkAfterFailedAcquire method is called in an infinite for loop in the acquireQueued method:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = ();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                 = null; // help GC
                failed = false;
                return interrupted;
            }
            //shouldParkAfterFailedAcquireIf the returntrue,then it will execute the
            //parkAndCheckInterruptmethodologies
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

As you can see from the code, once the shouldParkAfterFailedAcquire method returns true, the parkAndCheckInterrupt method will be executed and the thread of execution hangs.

8, the thread hangs: parkAndCheckInterrupt

private final boolean parkAndCheckInterrupt() {
    (this);
    return ();
}

This method is very simple and does two things:

  1. Hang the current thread
  2. Determine if an interrupt occurred in the current thread after being woken up.

Note the method called here:This interrupt method will reset the interrupt state after it is called, for example, if the interrupt state of the thread is already true, the first call to the () method will return true, and the second call to the () method will return false, for more details, you can refer to thejava concurrent programming: thread interrupt method interrupt details》。

Why?

First of all, it's important to understand thatin the event thatinterrupt statusis true, thenparkunblockableLockSupport's park method will stop blocking immediately in response to an interrupt if it encounters an interrupt, and it will not be able to block the thread if it is interrupted. acquireQueued is an interrupt-independent method, and its purpose is to grab a lock, and it is up to the caller to determine whether or not an interrupt is going to be executed, so it defines a local variableinterruptedto store the thread interrupt status temporarily and tell the caller the interrupt status when the thread acquires the lock.

In order to prevent the thread from entering the critical zone without blocking and destroying thread security, it is necessary to reset the thread's interrupt status to false, so that the next time the lock grab fails to call LockSupport's park method again, the thread can be successfully blocked. So this call to the () method is extremely clever: on the one hand, it queries the actual interrupt state, and on the other hand, it resets the interrupt state to false.

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = ();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                 = null;
                failed = false;
                //Return to the real interrupt status after acquiring the lock,Let the caller perform an interrupt
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //If an interrupt is detected,Storing interrupt status in local variables
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

The complete flow of the spin preemption lock (acquireQueued) is shown below:

image-20241217155055416

9. Restore interrupt status: selfInterrupt()

Going back to the very first call to the acquire method

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

The acquireQueued method returns the thread's interrupt status checked after the thread has acquired the lock. true means that an interrupt has occurred, false means that no interrupt has occurred. If an interrupt is detected, the selfInterrupt method is executed:

static void selfInterrupt() {
    ().interrupt();
}

This method does one thing: it executes the current thread's interrupt() method, which actually sets an interrupt flag. Setting the interrupt flag to true does not immediately terminate the thread unless it is in a waiting state.

This actually restores the thread's interrupted state, and when the thread encounters a method call such as sleep during execution, it will throw an InterruptedException to realize the thread interruption.

IV. Principles of Fair Lock Lock Release

The lock release for a fair lock is called from the unlock method of the ReentrantLock.

public void unlock() {
    (1);
}

It calls the release method of the FairSyn class, but FairSyn doesn't override the release method, it actually calls the release method of AQS directly

1. AQS template method: release

/**
 * This method is the lock release method in exclusive mode.
 */
public final boolean release(int arg) {
    // Try to release the lock resource.
    if (tryRelease(arg)) {
        Node h = head.
        if (h ! = null && ! = 0)
            // wake up the successor node to acquire the lock
            unparkSuccessor(h);
        return true;
    }
    return false; }
}

In the release method, you first need to call the tryRelease(arg) method to release the lock resource, note that the tryRelease method returns true/false, then the question arises, when to return true, when to return false?

Here to determine the conclusion: when the AQS lock status is 0 return true, otherwise return false, this actually has something to do with the reentrant lock, ReentrantLock is a reentrant lock, the code example is shown below

public class AQSDemo {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock(true);
        ();//statechange into1
        try {
            ();//statechange into2
            try {
                ("reentrant lock1");
            } finally {
                ();//statechange into1
            }
            ("reentrant lock2");
        } finally {
            ();//statechange into0
        }
    }
}

After two locks, the state value of AQS has become 2, and the first lock release state value will become 1. Should we wake up the successor node to seize the lock at this time? The answer is no, it is definitely necessary to completely release the lock before waking up the successor node to seize the lock.

2、Hook method implementation: tryRelease

The tryRelease method is a method of the Syn class, which is not overridden by the fair lock class FairSync

protected final boolean tryRelease(int releases) {
    // Calculate the state after the lock is released.
    int c = getState() - releases; //If the thread releasing the lock is not the thread holding the lock, throw an exception.
    // If the thread releasing the lock is not the thread holding the lock, throw an exception
    if (() ! = getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();; // If the thread releasing the lock is not the thread holding the lock, report the exception.
    boolean free = false;
    // Check if the state value is 0 after releasing the lock
    if (c == 0) {
        //If it's 0, the lock is completely released.
        free = true; //The thread that holds the lock will be released to the thread that holds the lock.
        // Mark the thread holding the lock as null.
        setExclusiveOwnerThread(null);
    }
    // Update the state value
    setState(c);
    return free; }
}

The tryRelease method is simple, recalculating and updating the state value on the one hand, and marking the thread currently acquiring the lock as null on the other.

As mentioned before, this method only returns true when state is 0, indicating that it is time to wake up subsequent nodes in the waiting queue.

Finally, note one detail: the method of updating the state value is to call the setState method, obviously when you grab the lock, you call the compareAndSetState method. This is because when the lock is released, the lock has been acquired exclusively by the current thread, so when the lock is released, there will be no thread security issues, there is no need to call the CAS method to set the state.

3. Wake up the successor node: unparkSuccessor

The unparkSuccessor method is used to wake up the successor node in the waiting queue.

/**
 * @param node The node here is the head node
 */
private void unparkSuccessor(Node node) {
    /*
     * Checks if the head node is negative, and if it is, tries to update it to zero.
     */
    int ws = ;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Normally the next node from the head node is the node that will be woken up, but it is possible that the next node
     * is canceled, then we need to traverse from the tail to the head to find the first successor node that has not been canceled as the
     * the node that really needs to be woken up.
     */
    Node s = ;
    if (s == null || > 0) {
        s = null; for (Node t = tail; )
        for (Node t = tail; t ! = null && t ! = node; t = )
            if ( <= 0)
                s = t;
    }
    if (s ! = null)
        //wake up the successor node
        ();
}

After this method is executed, the successor node is woken up and will continue to execute theparkAndCheckInterruptmethod, check for interrupts, and then add theacquireQueuedThe next for loop is performed in the method to try to grab the lock, and if the lock grab fails, it will continue to block; if the lock grab succeeds, the interrupt state will be restored if the interrupt was detected before.

But there are two doubts about this method:

First query: when the node wait status is found to be less than 0, why update the waitStatus status to 0

if (ws < 0)
    compareAndSetWaitStatus(node, ws, 0);

We know that the initial value of the Node node's waitStatus status is 0, and its value does not change until it enters the AQS queue and waits to acquire the lock; however, once there is a successor node, the successor node will determine the status of the predecessor node in the process of spinning up the lock, and change the predecessor node's status to -1: SIGNAL status, if it is 0, -2, or -3. The successor node then enters the pending state.

Now the head node waitStatus is changed to 0, then after waking up the succeeding node, if the succeeding node fails to grab the lock, it will still change the waitStatus of the predecessor node to -1, and then enter the pending state, so no one will be able to wake up the node. Of course, in a fair lock, the successor node will be able to acquire the lock after being woken up.

So why change waitStatus to 0?

Second question: why traverse the AQS queue from the tail toward the front?

Node s = ;
if (s == null ||  > 0) {
    s = null;
    for (Node t = tail; t != null && t != node; t = )
        if ( <= 0)
            s = t;
}

AQS queue is a bidirectional queue, why not directly from the current node query next node? From the code logic, the forward query from the tail is to prevent the case of NULL nodes, do not understand why there will be NULL nodes.


Finally, feel free to follow my blog yah: