Reentrantlock. java concurrency Implementation principle of condition

Posted by CoffeeOD on Fri, 21 Jan 2022 12:32:55 +0100

In general, we often encounter a situation in the actual development process that we can continue only after meeting a condition. For example, when we use the production consumption model, the consumption service must have data consumption. If there is no data, wait. When the production thread generates data, wake up the consumption thread. The example code is as follows:

ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        new Thread(()->
            {

                try{
                    lock.lock();
                    // do something A1
                   condition.await();
                   // do something A2
                }catch(Exception e){

                }
                finally{
                    lock.unlock();
                }
            }
        ).start();

        new Thread(()->
        {
            try{
                lock.lock();
                // do something A1
                condition.signal();
                // do something A2
            }catch(Exception e){

            }
            finally{
                lock.unlock();
            }
        }
        ).start();

Here we use the Condition in the contract as the Condition team. Let's look at the underlying implementation logic when we call lock When newcondition(),

public Condition newCondition() {
        return sync.newCondition();
    }
final ConditionObject newCondition() {
            return new ConditionObject();
        }

ConditionObject is defined in AQS.

public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        /** First node of condition queue. */
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        private transient Node lastWaiter;
}

You can see that a linked list is maintained internally in ConditionObject, reusing the Node data types in AQS.

When we call await, the implementation is as follows:

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            // Add a new CONDITION Node to the tail of the CONDITION team. Here, the waitStatus of the Node node will be set to CONDITION    
            Node node = addConditionWaiter();
            // This step is very important. When we call await, the current thread must have obtained the lock. Based on the previous analysis, when we obtain the lock,
            // The state variable in AQS will change. Here, the lock resources obtained by the current thread will be obtained first, that is, the state variable,
            // In this way, after re acquiring the lock, you need to restore the current thread lock resource to the level before await.
            // The logic of fullyRelease is: the state will be set to 0, that is, all lock resources of the current thread will be released. Here, the release method of AQS will be called,
            // During release, if the head node is a node from the condition queue, the thread of this node will be awakened
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // Whether the node is on the synchronization queue, is not on the synchronization queue, is blocked, and if it is still on the synchronization queue, it cannot be locked,
            // There may be synchronization queues. One is that signal s from other threads are immediately transferred to the synchronization queue after being placed in the condition queue,
            // Another is to wake up from sleep in the conditional queue and join the synchronization queue
            while (!isOnSyncQueue(node)) {
            	// If you are not on the synchronous match, you can use locksupport Park puts the current thread to sleep
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // At this time, the current thread has been awakened by signal, and the current Node is transferred from the condition queue to the synchronization queue to start re requesting to obtain the lock
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

As can be seen from the above, the main logic of await is as follows:

  1. First, add the current thread to the tail node of the condition queue through the addConditionWaiter method
  2. Release the resources held by the current thread, and wake up the head node in the synchronization queue to wait for the thread
  3. If the current node is not on the synchronization queue, the current thread will be blocked waiting
  4. The current thread is awakened (at this time, the current node will be transferred from the condition queue to the synchronization queue), and the lock is obtained through acquirequeueueueueueueueued. This logic is the same as the lock request

Next, let's look at the logic of signal:

public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }
private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }
final boolean transferForSignal(Node node) {
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
	    // Transfer the node on the condition queue to the synchronization queue
        Node p = enq(node);
        // enq returns the previous node of the current node after the current node is added to the synchronization queue
        int ws = p.waitStatus;
        // If the previous node of the current node in the WS > 0 table name synchronization queue is cancelled, or compareAndSetWaitStatus fails, the thread of the current node will be awakened
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

It can be seen from here that the main logic in signal is to transfer the nodes in normal status in the condition queue to the synchronization queue. The specific steps are as follows:

  1. Judge whether the operation node is normal through compareAndSetWaitStatus(node, Node.CONDITION, 0). Generally, the node status in the synchronization queue will not change after initialization (CONDITION) unless the wait is cancelled. Here, through cas operation, if the operation fails, the thread of the current node cancels the wait and ignores the node, Continue to find available nodes behind the linked list
  2. Transfer the current node from the condition queue to the synchronization queue through enq
  3. enq returns the previous node of the current node in the linked list, which is similar to the logic of the lock. If the waitStatus of the previous node is > 0 or setting the waitStatus of the previous node to SIGNAL fails, there is a problem with the previous node of the table name, and the thread of the current node will be awakened directly
  4. End of operation

Here we need to note that the signal method only transfers the nodes in the condition queue to the synchronization queue. At this time, the thread calling signal does not lock the resources. We must wait for the calling signal thread to release the resources, and the subsequent await threads can continue to obtain the resources for execution.
lock.newCondition() returns a new Condition every time, and await and signal must be on the same Condition. Each Condition will maintain its own Condition queue. Therefore, it can be said that there is only one synchronization queue in ReentrantLock, but there may be multiple Condition queues.

In addition, according to the previous ReentrantLock and Condition analysis here, many functions are reused in the implementation. Lock and Condition waiting are separated by synchronizing queue and Condition queue.

In addition, the thread status of the node is judged by the waitStatus of the node.

Topics: Java Back-end