Read JDK source code: exclusive mode in AQS

Time:2021-11-30

Abstract queued synchronizer, AQS for short, is a framework for building locks and synchronizers.
Common locking tools under the JUC package, such as reentrantlock, reentrantreadwritelock, semaphore and countdownlatch, are implemented based on AQS.
This paper will introduce the data structure of AQS and the implementation principle of exclusive mode.

This paper is based on jdk1.8.0_ ninety-one

1. AQS framework

All operations of AQS are carried out around the synchronization state, which solves the problems of mutual exclusion and synchronization of resource access.

  • It supports exclusive and shared access to resources.
  • For threads that cannot get resources, wait in the synchronization queue until they get resources successfully (or timeout, interrupt) and get out of the queue.
  • After the thread successfully obtains the resource, if the specified condition is not true, it releases the resource and enters the condition queue to wait until it is awakened, and then transfers to the synchronization queue to wait for obtaining the resource again.

The AQS framework leaves the remaining problem to users: the specific ways and results of obtaining and releasing resources.
This is actually a typical template method design pattern: the parent class (AQS framework) defines the skeleton and internal operation details, and the specific rules are implemented by the child classes.

1.1 inheritance system

Read JDK source code: exclusive mode in AQS

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable

Abstractqueuedsynchronizer inherits abstractownablesynchronizer, which has the attribute exclusiveownerthread, which is used to record threads that obtain locks in exclusive mode.

public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
    /**
     * The current owner of exclusive mode synchronization.
     */
    private transient Thread exclusiveOwnerThread;
}    

Abstractqueuedsynchronizer has two internal classes: conditionobject and node.

  • Conditionobject is the implementation of the condition interface and can be used with lock.
  • Node is the node of synchronization queue and condition queue in AQS.

1.2 formwork method

AQS defines a series of template methods as follows:

//Exclusive access (number of resources)
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

//Exclusive release (number of resources)
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

//Shared acquisition (number of resources)
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

//Shared acquisition (number of resources)
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}

//Exclusive status
protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}

The locking tools commonly used in Java are implemented based on AQS.

Read JDK source code: exclusive mode in AQS

2. Data structure

2.1 resource definition

Lock and resource are the same concept. They are the objects that multiple threads compete for.
AQS uses state to represent resources / locks, and completes the queuing work of obtaining resources / locks through the built-in waiting queue.
Wait queue is a strict FIFO queue and a variant of CLH lock queue.

Read JDK source code: exclusive mode in AQS

Since state is shared, volatile is used to ensure its visibility and providegetState/setState/compareAndSetStateThere are three methods to manipulate state.

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

/**
 * Returns the current value of synchronization state.
 * This operation has memory semantics of a {@code volatile} read.
 * @return current state value
 */
protected final int getState() {
    return state;
}

/**
 * Sets the value of synchronization state.
 * This operation has memory semantics of a {@code volatile} write.
 * @param newState the new state value
 */
protected final void setState(int newState) {
    state = newState;
}

/**
 * Atomically sets synchronization state to the given updated
 * value if the current state value equals the expected value.
 * This operation has memory semantics of a {@code volatile} read
 * and write.
 *
 * @param expect the expected value
 * @param update the new value
 * @return {@code true} if successful. False return indicates that the actual
 *         value was not equal to the expected value.
 */
Protected final Boolean compareandsetstate (int expect, int update) {// atomic operation
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

2.2 node definition

AQS implements two queues: synchronization queue and condition queue. Both queues use node as the node.

The node definition mainly includes three parts:

  1. Node status: signal, canceled, condition, propagate, 0.
  2. Node mode: nodes in the synchronization queue have two modes: shared and exclusive.
  3. Node pointing: the synchronization queue is a two-way linked list (prev / next), and the condition queue is a one-way linked list (nextwaiter).

Status of the node

  • Cancelled: a value of 1 indicates that the current node is cancelled due to timeout or interrupt.
  • Signal: the value is – 1. It is a wake-up signal, indicating that the successor node of the current node is waiting to obtain the lock. When a node in this state releases or cancels, it needs to execute unpark to wake up the successor node.
  • Condition: a value of – 2 indicates that the current node is a condition queue node, and the node synchronizing the queue will not have this state. When a node moves from the condition queue to the synchronization queue, the status is initialized to 0.
  • Propagate: the value is – 3. Only in the sharing mode can the head node of the synchronization queue be set to this state (see doreleaseshared), indicating that subsequent nodes can initiate the operation of obtaining shared resources.
  • 0: initial state, indicating that the current node is in the synchronization queue and waiting to obtain the lock.

java.util.concurrent.locks.AbstractQueuedSynchronizer.Node

static final class Node {
    /** Marker to indicate a node is waiting in shared mode */
    static final Node SHARED = new Node();
    /** Marker to indicate a node is waiting in exclusive mode */
    static final Node EXCLUSIVE = null;

    /** waitStatus value to indicate thread has cancelled */
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    static final int SIGNAL    = -1;
    /** waitStatus value to indicate thread is waiting on condition */
    static final int CONDITION = -2;
    /** waitStatus value to indicate the next acquireShared should unconditionally propagate */ 
    static final int PROPAGATE = -3;

    //Waiting status: signal, canceled, condition, propagate, 0
    volatile int waitStatus;

    //Points to the previous node in the synchronization queue
    volatile Node prev;

    //Points to the next node in the synchronization queue
    volatile Node next;

    volatile Thread thread;

    //In the synchronization queue, nextwaiter is used to mark the modes of nodes: exclusive and shared
    //In the condition queue, nextwaiter points to the next node in the condition queue
    Node nextWaiter;

    /**
     * Returns true if node is waiting in shared mode.
     */
    //Is the node mode shared
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    /**
     * Returns previous node, or throws NullPointerException if null.
     * Use when predecessor cannot be null.  The null check could
     * be elided, but is present to help the VM.
     *
     * @return the predecessor of this node
     */
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {    // Used to establish initial head or SHARED marker
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;          
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

2.3 synchronization queue

Read JDK source code: exclusive mode in AQS
The synchronization queue is a queue waiting to acquire a lock. It is a two-way linked list (prev / next). It uses head / tail to execute the head and tail nodes of the queue.

java.util.concurrent.locks.AbstractQueuedSynchronizer

/**
 * Head of the wait queue, lazily initialized.  Except for         
 * initialization, it is modified only via method setHead.  Note:
 * If head exists, its waitStatus is guaranteed not to be          
 * CANCELLED.
 */
//Wait for the head node of the queue to initialize. 
//Note that if the header node exists, its waitstatus must not be cancelled
private transient volatile Node head;

/**
 * Tail of the wait queue, lazily initialized.  Modified only via  
 * method enq to add new wait node.                                
 */
//Wait for the tail node of the queue to initialize. 
//New nodes can only be added to the waiting queue through the enq method.
private transient volatile Node tail;

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

After the thread fails to obtain resources, it will enter the tail of the synchronization queue and set a wake-up signal to the previous nodeLockSupport.park(this)Let itself enter the waiting state until it is awakened by the predecessor node.

When the thread waits in the synchronization queue and obtains resources successfully, it executessetHead(node)Set itself as the head node.
The head node of the synchronization queue is a dummy node, and its thread is empty (in some cases, it can be regarded as representing the thread currently holding the lock).

/**
 * Sets head of queue to be node, thus dequeuing. Called only by
 * acquire methods.  Also nulls out unused fields for sake of GC
 * and to suppress unnecessary signals and traversals.
 *
 * @param node the node
 */
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

AQS does not build an empty dummy node when initializing the queue, but when contention occurs for the first time:
When the first thread acquires the lock and the second thread fails to acquire the lock, it will initialize the queue, construct an empty node and point the head / tail to the empty node.
See abstractqueuedsynchronizer #enq.

2.4 conditional queue

Read JDK source code: exclusive mode in AQS

A condition queue is a queue that waits for conditions to be established. It is a one-way linked list (nextwaiter). It uses firstwaiter / lastwaiter to point to the beginning and end nodes of the queue.

java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject

/** First node of condition queue. */
private transient Node firstWaiter; //  Header node of condition queue
/** Last node of condition queue. */
private transient Node lastWaiter;  //  Tail node of condition queue

When the thread acquires the lock successfully, it executes session. Await() to release the lock and wait in the condition queue until other threads execute session. Signal to wake up the current thread.

After the current thread is awakened, it is transferred from the condition queue to the synchronization queue and waits again to obtain the lock.

3. Exclusive mode

In exclusive mode, as long as one thread holds the lock, other threads will not succeed in trying to obtain the lock.

3.1 acquire lock

Obtain lock / resource in exclusive mode, ignore interrupt, and the internal implementation of lock #lock

java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire

public final void acquire(int arg) { 
    if (!tryAcquire(arg) &&                            
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 
        selfInterrupt();
}
  1. Tryacquire: try to obtain the resource / lock directly. If it succeeds, it will return directly. If it fails, go to the next step;
  2. Addwaiter: after failed to obtain the resource / lock, add the current thread to the tail of the synchronization queue, mark it as exclusive mode, and return the newly queued node;
  3. Acquirequeueueueued: enables the thread to wait in the synchronization queue to obtain resources and return only after obtaining them. If it is interrupted during the waiting process, it returns true; otherwise, it returns false.
  4. Self interrupt: if the thread is interrupted during the waiting process, the interrupt status is added after the resource is obtained successfully.

3.1.1 tryAcquire

Try to get resources, and return true successfully. The specific resource acquisition method is implemented by the user-defined synchronizer.

java.util.concurrent.locks.AbstractQueuedSynchronizer#tryAcquire

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

3.1.2 addWaiter

After failed to acquire the resource / lock, encapsulate the current thread as a new node, set the mode of the node (exclusive and shared), join the tail of the synchronization queue, and return the new node.

java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter

/**
 * Creates and enqueues node for current thread and given mode.
 *
 *@ param mode node.exclusive for exclusive, node.shared for shared // exclusive mode and shared mode
 * @return the new node
 */
//Join from the end of the queue
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        If (compareandsettail (PRED, node)) {// set a new tail node
            pred.next = node;
            return node;
        }
    }
    enq(node); //  Tail is empty, join the team
    return node; //  Returns the current new node
}

enq

Queue up from the end of the synchronization queue. If the queue does not exist, initialize it.

java.util.concurrent.locks.AbstractQueuedSynchronizer#enq

/**
 * Inserts node into queue, initializing if necessary. See picture above.
 * @param node the node to insert
 * @return node's predecessor
 */
Private node enq (final node) {// queue up from the end of the synchronization queue
    for (;;) {
        Node t = tail;
        If (t = = null) {// must initialize // if the queue is empty, create an empty node as the header node
            if (compareAndSetHead(new Node()))
                tail = head;                  //  After initialization, it does not return, but carries on the next cycle
        } else {
            node.prev = t;
            If (compareandsettail (T, node)) {// if the queue is not empty, the current node will be used as a new tail. // CAS fails, and tail bifurcation may occur. The next cycle will eliminate the bifurcation
                t.next = node;                //  Since it is not an atomic operation, the queue operation first sets the prev pointer and then the next pointer, which will lead to the failure to traverse the tail node through the next node in case of concurrency
                return t;                     //  Returns the previous node (old tail node) of the current node
            }
        }
    }
}

be careful:

  1. When contention occurs for the first time, the thread that fails to compete for the lock will join the queue, first construct a dummy node as the head / tail node to initialize the queue, and then join the queue from the end of the queue.
  2. When joining the queue, set the node.prev, tail and pred.next pointers successively, which is a non atomic operation.
  3. After setting prev, if CAS fails to set tail, it means that other threads are queued first. At this time, entering the next cycle will correct the direction of prev.
  4. Since queuing is a non atomic operation, it may not be possible to traverse from the head to the tail node through next in concurrent cases, but the complete queue can be accessed through prev forward traversal from the tail node.

3.1.3 acquireQueued

Spin in the synchronization queue and wait to obtain resources until successful, and return the interrupt state during the waiting period.

java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueued

/**
 * Acquires in exclusive uninterruptible mode for thread already in
 * queue. Used by condition wait methods as well as acquire.
 *
 * @param node the node
 * @param arg the acquire argument
 * @return {@code true} if interrupted while waiting
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            If (P = = head & & tryacquire (ARG)) {// if the previous node is the head node, it means that the thread of the current node can try to obtain lock resources
                //The lock is obtained successfully. The current node is used as a new head node, and the thread information in the current node is cleared (that is, the head node is a dummy node)
                //There will be no contention and CAS is not required
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //If the previous node is not the head node, or the thread of the current node fails to obtain the lock, you need to judge whether to enter the blocking:
            //1. If blocking cannot be entered, Retry to obtain the lock. 2. Access blocking
            if (shouldParkAfterFailedAcquire(p, node) &&
                Parkandcheckinterrupt()) // block the current thread. When you wake up from the blocking, check whether the current thread has been interrupted and clear the interrupt status. Then continue to retry obtaining the lock.
                interrupted = true;      //  Mark that the current thread has been interrupted (if the thread is awakened by an interrupt when blocking, it will retry to obtain the lock until it succeeds, and then respond to the interrupt)
        }
    } finally {
        If (failed) // an exception occurred during spin lock acquisition and blocking
            cancelAcquire(node); //  Cancel acquire lock
    }
}

In the acquirequeueueueued method, the thread mainly makes two judgments in spin:

  1. Can I get a lock
  2. Can I enter the block

Specific code flow:

  1. In the synchronization queue, if it is determined that the predecessor node is the head node, the current node attempts to obtain the lock.
  2. If the current thread obtains the lock successfully, set the current node as the head node and return the interrupt status of the current thread.
  3. If the current thread cannot acquire the lock and fails to acquire the lock, judge whether it enters the blocking.
  4. If you cannot enter the block, continue to spin, otherwise enter the block.
  5. After the thread is awakened from the block, check and mark the interrupt state of the thread and re-enter the spin.

shouldParkAfterFailedAcquire

After the current node fails to acquire the lock, it can judge whether the current node can enter the blocking state by verifying the waiting state of the previous node.
Return true to enter blocking; If false is returned, blocking is not allowed. You need to retry to obtain the lock.

java.util.concurrent.locks.AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire

/**
 * Checks and updates status for a node that failed to acquire. 
 * Returns true if thread should block. This is the main signal
 * control in all acquire loops.  Requires that pred == node.prev.
 *
 * @param pred node's predecessor holding status
 * @param node the node
 * @return {@code true} if thread should block
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         *This node has already set status asking a release // the current node has set a wake-up signal for its previous node
         *To signal it, so it can safely park
         */
        return true;
    if (ws > 0) {
        /*
         *Predecessor was cancelled. Skip over predecessors and // if the status of the previous node is greater than 0, it indicates that the status is cancelled and the current node will not be notified
         *Indicate retry. // go straight ahead to find a waiting node and line it up behind it
         */// the current node cannot enter the blocking. You need to retry obtaining the lock
        do {
            node.prev = pred = pred.prev; //  pred = pred.prev;  node.prev = pred; //  Skip the previous node until a node with waitstatus > 0 is found
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         *Waitstatus must be 0 or propagate. Indicate that we // the status of the previous node is equal to 0 or propagate, indicating that it is waiting to obtain locks / resources
         *Need a signal, but don't park yet. Caller will need to // at this time, you need to set the wake-up signal for the previous node, but do not block it directly
         * retry to make sure it cannot acquire before parking. // because before the blocking, the caller needs to retry to confirm that it really does not get resources.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL); //  Change the status of the previous node to signal through CAS
    }
    return false;
}

The condition that the current node can enter blocking is that there are other threads to wake it up.
By setting the status of the previous node to signal, it is ensured that the previous node can wake up the current node after releasing the lock.

There are three situations:

  1. The status of the previous node is node.signal, which indicates that the current node has the conditions to be awakened and can enter blocking.
  2. If the status of the previous node is cancelled, the current node will be placed behind the non cancelled node and continue to spin without entering blocking.
  3. If the status of the previous node is 0 or propagate, it indicates that it is waiting to obtain the lock, then the current node sets the previous node as signal and continues to spin without entering blocking.

parkAndCheckInterrupt

Enter blocking. After blocking, check the interrupt status.

java.util.concurrent.locks.AbstractQueuedSynchronizer#parkAndCheckInterrupt

/**
 * Convenience method to park and then check if interrupted
 *
 * @return {@code true} if interrupted
 */
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

cancelAcquire

If an exception occurs during the thread’s spin attempt to acquire the lock in acquirequeueueueued, it will execute cancelacquire in the finally code block to terminate the acquisition of the lock.

/**
 * Cancels an ongoing attempt to acquire.
 *
 * @param node the node
 */
Private void cancelacquire (node node) {// canceling lock acquisition
    // Ignore if node doesn't exist
    if (node == null)
        return;

    node.thread = null;

    //Skip cancelled predecessors // skip the canceled predecessor node and find a valid predecessor node for the current node
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    Node predNext = pred.next;

    //Can use unconditional write instead of CAS here. // write operations are volatile, so CAS is not required here
    //After this atomic step, other nodes can skip past us. // after the current node is set to cancelled, other nodes will skip the current node when looking for a valid predecessor node
    // Before, we are free of interference from other threads.
    node.waitStatus = Node.CANCELLED;

    // If we are the tail, remove ourselves.
    If (node = = tail & & compareandsettail (node, PRED)) {// if it is a tail node, it will be dequeued
        compareAndSetNext(pred, predNext, null);
    }Else {// enter here. It indicates that it is not a tail node, or it is a tail node, but the queue exit fails. You need to process the successor node
        //If successor needs signal, try to set PRED's next link // if the successor node needs to be notified, try to find a new successor node
        //So it will get one. Otherwise wake it up to propagate
        int ws;
        If (PRED! = head & & // the predecessor node is not a head node
            ((WS = PRED. Waitstatus) = = node. Signal | // the status of the predecessor node is signal or the status of the predecessor node is not cancelled and the attempt to set it to signal succeeds
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            If (next! = null & & next. Waitstatus < = 0) // subsequent nodes exist and are not cancelled
                compareAndSetNext(pred, predNext, next); //  Set a new predecessor node (i.e. the valid node found earlier) for the successor node, and the current node is out of the queue
        } else {
            unparkSuccessor(node); //  If there is a successor node, it means that a new successor node cannot be found for the successor node (maybe the predecessor node is head or the predecessor node fails) and wake up the successor node directly
        }

        node.next = node; // help GC
    }
}

The node node cancels obtaining the lock, indicating that the current node status has changed to cancelled and becomes an invalid node.

Consider how to handle the successor nodes of node node:

  1. If there is no successor node, set the last valid node (waitstatus < = 0) to tail.
  2. If there is a successor node, it needs to be hung after the last valid node, and then the node wakes up the successor node.
  3. If there is a successor node and no valid predecessor node is found, wake up the successor node directly.

unparkSuccessor

Wake up the successor node of the current node.

java.util.concurrent.locks.AbstractQueuedSynchronizer#unparkSuccessor

/**
 * Wakes up node's successor, if one exists.
 *
 * @param node the node
 */
//Wake up the successor node of the current node 
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus; //  If the status of the current node is cancelled, it remains unchanged; If it is less than 0 (it is possible that the successor node needs the current node to wake up), it is cleared.
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0); //  It doesn't matter if CAS fails (it means that the thread of the successor node first modifies the state of the current node), because the successor node will wake up manually next

    Node s = node.next;
    If (s = = null | s.waitstatus > 0) {// if the subsequent node is empty or cancelled, traverse the valid node forward from tail
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t; ////  be careful! After finding it here, there is no return, but continue to look forward
    }
    if (s != null)
        LockSupport.unpark(s.thread); //  Wake up the thread of the successor node (or the valid node closest to the head node in the queue)
}

Usually, the node to wake up is its own successor node. If the successor node exists and is waiting for a lock, wake it up directly.
However, it is possible that the subsequent node cancels the waiting lock. At this time, start from the tail node to find the UN cancelled node closest to the head node and wake it up.

Why not traverse the valid node backward from the current node?

  1. The current node may be the tail node, and there is no successor node.
  2. When joining the queue, set the prev pointer first and then the next pointer (see abstractqueuedsynchronizer #enq). It is a non atomic operation. It is more accurate to traverse forward according to the prev pointer.

3.2 acquire lock

Compared with acquire, the two processes interrupt during lock acquisition are different.

  1. Acquire interrupts the process of waiting for the lock. It will wait until the lock is obtained successfully before processing the interrupt.
  2. If the process of acquiring the lock is interrupted, the interruptedexception will be thrown immediately and the lock will no longer be acquired.

java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireInterruptibly

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

java.util.concurrent.locks.AbstractQueuedSynchronizer#doAcquireInterruptibly

/**
 * Acquires in exclusive interruptible mode.
 * @param arg the acquire argument
 */
private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException(); //  When a thread is awakened by an interrupt while blocking the waiting lock, it will give up the waiting lock and throw an exception directly
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

3.3 release lock

Releasing lock / resource in exclusive mode is the internal implementation of lock #unlock.

java.util.concurrent.locks.AbstractQueuedSynchronizer#release

public final boolean release(int arg) {
    If (tryrelease (ARG)) {// release lock resource
        Node h = head;
        If (H! = null & & h.waitstatus! = 0) // head.waitstatus = = 0, indicating that there is no node to wake up after the head node
            unparkSuccessor(h); //  Wake up the successor node of the head
        return true;
    }
    return false;
}
  1. Tryrelease: release the lock resource. If the release is successful, proceed to the next step.
  2. Unparksuccess: if the head node of the synchronization queue exists and meets waitstatus= 0, wake up the successor node.

java.util.concurrent.locks.AbstractQueuedSynchronizer#tryRelease

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

Status of head node H:

  • h. Waitstatus = = 0, the initial status of the node in the synchronization queue is 0, indicating that there is no successor node to wake up.
  • h. Waitstatus < 0. In exclusive mode, it indicates that it is in signal state. At this time, there are subsequent nodes waiting to wake up. See abstractqueuedsynchronizer #shouldparkafterfailedacquire.
  • h. If waitstatus > 0, it indicates that it is in the cancelled state. The head node is from the node that cancels obtaining the lock. To be safe, check whether there is a successor node that needs to be awakened.

Related reading:
Read JDK source code: exclusive mode in AQS
Read JDK source code: Sharing Mode in AQS
Read JDK source code: AQS implementation of condition

Author:Sumkor