ReentrantLock and Condition deep parsing

preface

I wrote an article about AQS and talked about the source code analysis of ReetrantLock. The main purpose here is to write ReentrantLock in combination with the await and signal of the source code analysis Condition, so AQS still needs to be mentioned.

1, Several important attributes in AQS

  1. AQS maintains a very important variable state, which is of type int, indicating the locked state, and the initial state value is 0;
  2. In addition, AQS also maintains a very important variable exclusiveOwnerThread, which represents the thread that obtains the lock, also known as the exclusive thread. The reentrant in ReentrantLock uses this attribute when state= 0 but exclusiveOwnerThread = = the current thread, then you can re-enter without going through the logic of obtaining the lock again.
  3. AQS also has a queue used to store threads that failed to acquire locks. It is a double linked list structure of FIFO, as well as head and tail nodes.
  4. The Node node of the double linked list contains the following main attributes:

Node: SHARED and EXCLUSIVE: used to identify whether the node is in SHARED mode or EXCLUSIVE mode

int: waitStatus: identifies the waiting status of the node. There are the following values:

CANCELLED: a value of 1 indicates that the node has been CANCELLED. For example, when tryLock(1000, TimeUnit.MILLISECONDS) is used to obtain a lock, the timeout will be CANCELLED

SIGNAL: a value of - 1 indicates that the thread of the successor node of this node has been suspended through LockSupport.park(). The previous node needs to wake up the thread when releasing the lock or canceling

condition: a value of - 2 indicates that the node is in the condition queue. Generally, it is operated through condition.await(), and then the node is inserted into the tail of condition

PROPAGATE: if the value is - 3, the header node of the sharing mode may be in this state, indicating that it propagates downward. Suppose two threads release the lock at the same time. Through competition, one of them is responsible for waking up the successor node, while the other sets the head node to this state. After the new node wakes up, it wakes up the next node directly according to this state of the head node.

Note that when new Node(), waitStatus is equal to 0 by default.

2, Structure of ReetrantLock

  1. ReetrantLock includes fair locks and non fair locks. The default is non fair locks.
  2. ReetrantLock contains an abstract internal class: Sync, which inherits AQS, as follows:
 abstract static class Sync extends AbstractQueuedSynchronizer
  1. Fair and unfair locks in ReetrantLock inherit from Sync, respectively

3, Analysis of obtaining and releasing locks of ReetrantLock

Here we will analyze the source code of the default unfair lock, version jdk1.8.

ReentrantLock lock = new ReentrantLock()

try {
   lock.lock()
   ...
} finally {
  lock.unLock()
}

Unlike synchronized, ReentrantLock does not actively release the lock, so you need to actively call unLock release in the finally block.

Here, it is assumed that four threads A, B, C and d call lock to obtain the lock at the same time, and the tasks performed by each thread after obtaining the lock are very time-consuming.

1. lock.lock()
  1. The lock method of ReentrantLock will be called, as follows:
    public void lock() {
        sync.lock();
    }

Since the default is a non fair lock, the lock method of NonfairSync is called, as follows:

final void lock() {
    // The default value of state is 0, so the cas operation returns true when the first thread obtains the lock
    // When the thread releases the lock, it will set the state to 0 again
    if (compareAndSetState(0, 1)) // Note [1]
        // Set the thread that acquires the lock as the exclusive thread
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1); // Note [2]
 }

Since thread A is the first to request A lock, set the AQS state value to 1 through CAS, indicating that thread A has obtained the lock, and then

At this time, when thread B obtains the lock again, it goes to comment [1]. At this time, the value of state is 1, so the cas operation will fail. Then it goes to comment [2]. acquire is the method in AQS, as follows:

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

The above method contains three important steps:

  1. tryAcquire: try to acquire the lock again. This method is in the internal class NonfairSync of ReentrantLock.
  2. addWaiter: convert the current thread into a Node node and insert it into the end of the waiting queue. If it is the first Node inserted into the waiting queue, it also needs a new empty Node as the head
    Node.
  3. Acquirequeueueueued: this method determines whether the precursor node of the node node is the head node. If so, try to obtain the lock again. If the lock is obtained successfully, set the current node as the head node; If the lock acquisition fails, the current thread is blocked (suspended).

tryAcquire

protected final boolean tryAcquire(int acquires) {
     return nonfairTryAcquire(acquires);
}
       final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            // Indicates that the thread that previously acquired the lock has released the lock
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // Indicates that the thread that previously acquired the lock did not release the lock, and then determines whether the current thread is the thread that previously acquired the lock
            // If yes, then state + 1, here is the re-entry of the lock
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

If tryAcquire succeeds in obtaining the lock, it returns true; otherwise, it returns false. Let's take a look at addWaiter(Node.EXCLUSIVE). Node.EXCLUSIVE, as mentioned earlier, refers to an exclusive lock. The code is as follows:

    private Node addWaiter(Node mode) {
        // new Node, the thread of the Node is assigned as the current thread
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        // When the first thread that fails to acquire a lock comes here, both head and tail are equal to null
        if (pred != null) { 
        // Not equal to null, indicating that there are waiting threads in the waiting queue,
        // The following operation is to insert the node into the end of the queue and then return to the node 
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // If you go here, it means that the waiting queue is empty
        enq(node);
        return node;
    }

Since thread B is the first thread that has not obtained the lock, and both head and tail are null, go to enq(node):

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail; //Note [1]
            if (t == null) { // Note [2]
                if (compareAndSetHead(new Node())) //Note [3]
                    tail = head; //Note [4]
            } else {
                node.prev = t; //Note [5]
                if (compareAndSetTail(t, node)) { //Note [6]
                    t.next = node; //Note [7]
                    return t; //Note [8]
                }
            }
        }
    }

Draw a picture here to show:
Note [1]: since tail == null, note [2] holds;
Note [3]: new an empty node, and then set the empty node to head through cas operation. At this time, the waitStatus value of the head node is 0 (the default value)
Note [4]: let tail point to head, and the effect after execution is as follows:

At this time, both head and tail point to an empty node, in which the corresponding thread is null, and waitStatus = 0;
The for loop will go to note [1] again, and t is assigned to tail, so the condition of note [2] is not tenable. Go to note [5], that is, prev of thread B points to t, that is, to tail, as shown in the following figure:

Then go to note [6] through cas operation, let tail point to the node corresponding to thread B.
Note [7] is used to point the next pointer of t to the node, and then return to the node, as follows:


At this point, the addWaiter(Node mode) analysis is completed. Let's talk about acquirequeueueueued (final node, int ARG). The code is as follows:

   final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor(); // Note [1]
                if (p == head && tryAcquire(arg)) { // Note [2]
                    setHead(node); // Note [3]
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())// Note [4]
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

Note that this is also an infinite loop operation. At this time, thread B is already in the waiting queue:

Note [1]: at this time, the precursor node of node node is head, so tryAcquire is executed. Because thread A performs time-consuming operations and will not release the lock temporarily, the tryAcquire condition in note [2] is not satisfied. Go to note [4].

Note [4]: shouldParkAfterFailedAcquire and parkAndCheckInterrupt are included. Let's analyze them one by one. Let's look at shouldParkAfterFailedAcquire first. The method is as follows:

   private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL) // Note [1]
            /***
            * Indicates that it has been set to SIGNAL. When this pred node releases the lock
            * It can wake up the thread corresponding to the successor node of pred
            */
            return true;
        if (ws > 0) {
            // Indicates that the precursor node has been cancelled, then move forward from the pred node,
            // Delete all cancelled nodes from the waiting queue
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else { // Note [2]
            // Set the waitStatus of the pred node to SIGNAL through cas
            // Only when it is set to SIGNAL can subsequent nodes wake up
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

Next, the precursor node of the node corresponding to thread B is the head node. As mentioned earlier, the value of waitStatus of the head node is 0. All will go to the comment [2], set waitStatus = -1 (SIGNAL = -1) of the head, return false, return to the acquirequeueueueued method, and continue to execute the for loop. Because thread A still does not release the lock, Or go to shouldParkAfterFailedAcquire method. At this time, the waitStatus of pred = signal, which has returned true. Then execute the parkAndCheckInterrupt method in acquirequeueueued, as follows:

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

This method is very simple. Call LockSupport.park to block thread B.
Above, after execution, the value of waitStatus of head is - 1. As shown below:

At this point, the lock acquisition operation of thread B is completed and blocked. Now it's thread C's turn to request the lock. Is the process the same or finally here

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

Since thread A did not release the lock, thread C failed to obtain the lock tryAcquire, and then went to addWaiter. At this time, the node corresponding to thread C was inserted into the tail of the waiting queue, as shown in the following figure:

Then go to acquirequeueueued. I have analyzed the code before. Here I will briefly talk about the results. Here, we will first judge whether the precursor node of thread C node is the head node. Obviously, it is not. The precursor node of thread C node is the node corresponding to thread B, so we will go to shouldParkAfterFailedAcquire(pred, node). Similarly, after two cycles, set the waitStatus value of the node corresponding to thread B to 1, and then block thread B. As shown in the figure below:

Similarly, the operation of thread D is the same as that of thread C. after the lock acquisition process is completed, see the following figure:

It can be seen from the above analysis that the waitstatus of the non tail node of the waiting queue formed by ReentrantLock = - 1;

Summary of ReentrantLock's lock method:

  1. When a thread acquires a lock, it will set state to 1, and then set exclusiveOwnerThread to the current thread
  2. If the current thread obtains the lock again, it will increase the state by 1, that is, the lock will be re entered
  3. Other threads acquire the lock. If the lock is not released by other threads, the node corresponding to this thread will be inserted into the end of the waiting queue, and then judge whether the precursor node of the node corresponding to this thread is the head node. If it is the head node, it will try to acquire the lock. If it is obtained, it will directly return. If it is not obtained, the, Set the waitStatus value of the precursor node to SIGNAL(-1), and then block this thread; If it is not a head node, set the waitStatus value of the precursor node to SIGNAL(-1) by the same logic, and then block this thread. The corresponding flow chart is as follows:

Posted by ToonMariner on Thu, 02 Sep 2021 11:24:39 -0700