[source code analysis] reentrant lock analysis AQS of reentrant lock

Keywords: Java JUC

ReentrantLock

SynchronizedReentrantLock
Lock implementation mechanismObject header monitor modeRely on AQS
flexibilityInflexibleSupport response interrupt, timeout and attempt to acquire lock
Release lock formAutomatic release lockShow call unlock()
Support lock typeUnfair lockFair lock & unfair lock
Conditional queueSingle condition queueMultiple conditional queues
Is reentrant supportedsupportsupport

Write before:

Distinguish clearly: lock is an API open to the outside world, while AQS synchronizer is a container for thread management. Tasks are distinguished and AQS is studied.

(Lock: lock,unlock;AQS: acquire,acquireQueue......).

The final implementation of abstract synchronization queue is: Fair lock or unfair lock

Locking process:

Locking:

  • Lock through ReentrantLock's locking method.
  • The Lock method of the internal class Sync will be called. Because Sync#lock is an abstract method, the Lock method of the relevant internal class will be executed according to the fair Lock and unfair Lock selected by ReentrantLock initialization. In essence, the Acquire method of AQS will be executed.
  • The Acquire method of AQS will execute the tryAcquire method. However, since the tryAcquire requires a custom synchronizer implementation, the tryAcquire method in ReentrantLock is executed. Because ReentrantLock is a tryAcquire method implemented through internal classes of fair lock and unfair lock, different tryAcquire methods will be executed according to different lock types.
  • tryAcquire is the logic for obtaining locks. After the acquisition fails, the subsequent logic of the framework AQS will be executed, which has nothing to do with the ReentrantLock custom synchronizer.

Unlock:

  • Unlock through unlock method of ReentrantLock.
  • Unlock will call the Release method of the internal class Sync, which inherits from AQS.
  • The tryRelease method will be called in the Release. tryRelease requires a custom synchronizer implementation. tryRelease is only implemented in Sync in ReentrantLock. Therefore, it can be seen that the process of releasing a lock does not distinguish whether it is a fair lock or not.
  • After the release is successful, all processing is completed by the AQS framework, independent of the user-defined synchronizer.

state

state Status of lock: 0~n
state: volatile ,CAS
    Make sure that the variable is also visible to other threads.
    No lock mechanism change state Value of
    
1)When state=0 When, it indicates no lock state
2)When state>0 Indicates that a thread has obtained a lock, that is state=1,But because ReentrantLock Reentry is allowed, so when the same thread obtains the synchronization lock multiple times, state Will increase, for example, re-enter 5 times, then state=5.  When releasing the lock, it also needs to be released 5 times until state=0 Other threads are eligible for locks

AOS: Specifies the thread of the current exclusive lock.

Queues in AQS

characteristic:

1. First in first out dual ended queue

2. The queue structure is composed of Head and Tail nodes, and the visibility is guaranteed through volatile modification

3. The Node pointed to by the head is the Node that has obtained the lock. The Node pointed to by the head is a virtual Node, and the Node itself does not hold a specific thread; It can also be regarded as [locked Node + blocking queue]

4. If the synchronization status cannot be obtained, the node will be spin locked. After a certain number of spin failures, the thread will be blocked. Compared with the CLH queue, the performance is better [lock grabbing method: adaptive spin lock]

Status of the node

Key methods and attributesCorresponding meaning
waitStatusWhat state is the current node in the queue
threadRepresents the thread corresponding to the node
prevPrecursor pointer, pointing to the previous node of this node
nextSubsequent pointer to the next node of this node
predecessorReturn the precursor node, and throw an NPE exception if there is no one
Attribute valueValue meaning
0Default value of Node after initialization
CANCELLEDWith a value of 1, the node was cancelled due to an interrupt or timeout
SIGNALA value of - 1 indicates that the successor node of the node is about to be blocked; The current node needs to wake up its successor nodes
CONDITIONA value of - 2 indicates that the node is in the waiting queue and the node thread is waiting to wake up

Acquire lock_ Core approach:

  • Acquire: how to acquire locks
  • tryAcquire: it will try to acquire the lock through CAS again.
  • addWaiter: ① encapsulate the current thread as a Node node, ② add it to the bidirectional linked list (waiting queue) of the above lock (enq)
    1. If the tail node is not empty, you need to add the new node to the next node of oldTail, and point the prev node of the new node to oldTail;
    2. If the current queue is empty, initialization is required. At this time, both the head node and the tail node are h = new Node() instances; At this time, oldTail = h is not empty, prev of node is oldtail, and next of oldtail is node. (must succeed because of spin)
  • Acquirequeueueued: judge whether the front node of the current queue can obtain the lock by spinning
    1. The current node is the first in the queue (head.next). It is qualified to rob the lock. Try to acquire once, return interrupted = false, and exit;
    2. If the node is the first to join the waiting queue, the prev node of the node is head (New node()), and the node will acquire the lock first. After failure, because the waitStatus of prev is 0, set its waitStatus to - 1, and then cycle again. If the failure to acquire the lock again, it will call parkAndCheckInterrupt to block the current thread;
    3. During shouldParkAfterFailedAcquire, the node in the queue with CANCELLED = 1 will be deleted. That is to say, every time a node is added, after the lock acquisition fails, the queue may be sorted again. Judge whether the current node needs a park

tryAcquire

When trying to get the lock, there are only two situations that can succeed:

1) No lock

2) Own person (involving re-entry of lock)

protected final boolean tryAcquire(int acquires) {
    // Determine the lock status and whether the thread can re-enter
    final Thread current = Thread.currentThread();
    int c = getState();
    
    if (c == 0) {
        //Because fairSync is a fair lock, you need to check whether there are waiters in the queue before the current thread at any time
        if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
            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;
}

addWaiter

addWaiter: ① encapsulate the current thread as a Node node, ② add it to the bidirectional linked list (waiting queue) of the above lock (enq)

  1. If the tail node is not empty, you need to add the new node to the next node of oldTail, and point the prev node of the new node to oldTail;
  2. If the current queue is empty, initialization is required. At this time, both the head node and the tail node are h = new Node() instances; At this time, oldTail = h is not empty, prev of node is oldtail, and next of oldtail is node. (must succeed because of spin)

acquireQueued

Acquire lock (tryacquire) - > construct node (addwaiter) - > join queue (addwaiter) - > acquire lock (acquirequeueueueueued)

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    //Whether the current thread has been interrupted = whether it has been interrupted during the mark waiting process
    boolean interrupted = false;
    try {
        //Spin.. front Node.. Node either acquires a lock or interrupts
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;  //There is no exception during the lock acquisition process of the current thread
                return interrupted;  // This is the exit, the only exit
            }

            // Suspend the current thread and return the interrupt flag of the current thread after waking up
            // Wake up: 1. Wake up other threads unpark normally. 2. Other threads send an interrupt signal to the currently suspended thread
            if (shouldParkAfterFailedAcquire(p, node) &&  parkAndCheckInterrupt())
                //interrupted == true indicates that the thread corresponding to the current node is awakened by [interrupt signal...]
                interrupted = true;
        }
    } finally {
        //true indicates that the current thread successfully preempts the lock. Under normal circumstances, [lock] the current thread will get the lock sooner or later
   	    //false indicates failure, and the logic of dequeuing needs to be executed... (when responding to the interrupted lock method.)
        if (failed)
            cancelAcquire(node);
    }
}

shouldParkAfterFailedAcquire

1. pred (waitState) is 0, and the current node does not need to be blocked.;

2. pred (waitState) is 1, skip;

3. pred (waitState) is - 1 and park the current node.

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    
    // 1. If the front node is blocked, suspend the current thread (the lock application node blocks)
    if (ws == Node.SIGNAL)  return true;
    
    // 2. The queue contains the terminated nodes
    if (ws > 0) {
        do {
            // Skip all CANCELLED nodes = = dequeue all CANCELLED nodes
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
        
    // 3. Initialized Node. The default state is 0.
    } else {
        // Force the front node to SIGNAl, which means that the front node needs to wake me up after releasing the lock
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

Cancel task

public void unlock() {
    sync.release(1);
}

The main purpose here is to release resources, update the synchronization status, and then wake up the next waiting thread.

release

//AQS#release method
//Reentrantlock. Unlock() - > sync. Release() [release provided by AQS]
public final boolean release(int arg) {
    //Try to release the lock. tryRelease returns true, indicating that the current thread has completely released the lock
    //Returns false, indicating that the current thread has not completely released the lock
    if (tryRelease(arg)) {

        //When will head be created?
        //When the lock holding thread does not release the thread, and other threads want to obtain the lock during the lock holding period, other threads find that they cannot obtain the lock, and the queue is empty. At this time, subsequent threads will be in the current lock
        //Thread builds a head node, and then subsequent threads will be appended to the head node.
        Node h = head;

        //Condition 1: true, indicating that the head node in the queue has been initialized, and multiple thread contention has occurred during the use of ReentrantLock
        //Condition 2: if the condition is true, it means that the node node must have been inserted behind the current head.
        if (h != null && h.waitStatus != 0)
            //Wake up the successor node
            unparkSuccessor(h);
        return true;
    }

    return false;
}

tryRelease

//Sync#tryRelease()
protected final boolean tryRelease(int releases) {
    //Minus the released value
    int c = getState() - releases;
    //If the condition is true: it indicates that the current thread is not locked.. direct exception
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();

    //The current thread holds a lock


    //Whether the lock has been completely released.. the default is false
    boolean free = false;
    //Condition true: indicates that the current thread has reached the condition of completely releasing the lock. c == 0
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    //Update AQS.state value
    setState(c);
    return free;
}

unparkSuccessor

1) If the next node is empty or its waiting state is cancelled, look back, find a waiting state < = 0, and then wake it up;
2) If the next node is not empty and the waiting state is < = 0, wake it up.

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0) compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;

    
    //Condition 1:
    //When is s equal to null?
    //1. When the current node is the tail node, s == null.
    //2. When the queue entry of a new node is not completed (1. Set the prev of the new node to pred 2.cas, and set the new node to tail 3. (incomplete) pred.next - > new node)
    //You need to find a node that can be awakened

    //Condition 2: s.waitstatus > 0 premise: s= null
    //Established: indicates that the successor node of the current node is in the cancelled state... You need to find a suitable node that can be awakened
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }


    //If you find a suitable node that can be awakened, wake up.. do nothing if you can't find it.
    if (s != null)
        LockSupport.unpark(s.thread);
}

cancelAcquire

/**
 * The queue location of the currently unqueued node is different, and the out of queue strategy is different. There are three situations:
 * 1.The current node is tail - > node
 * 2.The current node is neither a head.next node nor a tail node
 * 3.The current node is the head.next node.
 */
private void cancelAcquire(Node node) {

    if (node == null) return;
    node.thread = null;
    Node pred = node.prev;
    //Filter out all invalid nodes pred.waitstatus > 0, i.e. 1 cancelled
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    Node predNext = pred.next;
    node.waitStatus = Node.CANCELLED;
    
    //Condition 1: node == tail established: current node is tail - > node
    //Condition 2: if compareAndSetTail(node, pred) succeeds, it indicates that the modification of tail is completed.
    if (node == tail && compareAndSetTail(node, pred)) {
        //Modify pred.next - > null. To complete node dequeue.
        compareAndSetNext(pred, predNext, null);

    } else {
        int ws;

        //Condition 1: PRED= If head is established, it means that the current node is neither the head.next node nor the tail [intermediate node]
        if (pred != head &&
            //Condition 2.1: true: indicates that the node's precursor status is Signal. False: the precursor status may be 0,
            ((ws = pred.waitStatus) == Node.SIGNAL ||
                // Assuming that the precursor state is < = 0, set the precursor state to Signal state.. it means to wake up the successor node.
                (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&pred.thread != null) {
            Node next = node.next;
            //Let pred.next - > node.next, so you need to ensure that the status of the pred node is Signal.
            if (next != null && next.waitStatus <= 0) compareAndSetNext(pred, predNext, next);

        } else {
            //head.next node, directly unpark the successor (equivalent to ignoring the current node)
            unparkSuccessor(node);
        }
        node.next = node; // help GC, the next pointer of node is modified, but the prev pointer remains unchanged
    }
}

Why do all changes operate on the Next pointer instead of the Prev pointer?

When cancelAcquire is executed, the front Node of the current Node may have been out of the queue (the shouldParkAfterFailedAcquire method in the Try code block has been executed). If the Prev pointer is modified at this time, it may lead to the Prev pointing to another Node whose queue has been removed. Therefore, this change of the Prev pointer is unsafe.

Under what circumstances does the Prev pointer operate?
In the shouldParkAfterFailedAcquire method, the following code will be executed, which is actually processing the Prev pointer. shouldParkAfterFailedAcquire is executed only when the lock acquisition fails. After entering this method, it indicates that the shared resources have been obtained and the nodes before the current node will not change. Therefore, it is safe to change the Prev pointer at this time.

do {
    node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);

Posted by sweenyman on Sun, 03 Oct 2021 20:21:47 -0700