ReentrantLock Source Parsing for Dead-end java Synchronization Series - Conditional Lock

Keywords: Programming Java

problem

(1) What is the conditional lock?

(2) What scenarios does conditional locking work for?

(3) Was await() conditionally locked await() awakened when other threads signal()?

brief introduction

Conditional locks are locks that are used when the current business scenario finds itself unable to process after acquiring a lock and needs to wait for a condition to occur before processing can continue.

For example, in a blocked queue, an element cannot be ejected when there are no elements in the queue. At this time, it needs to be blocked on the condition notEmpty, wait for other threads to put an element in it, wake up the condition notEmpty, and the current thread can continue to do the "eject an element" behavior.

Note that the condition here must wait after acquiring the lock, for a condition lock corresponding to ReentrantLock, or for the condition.await() method to be called after acquiring the lock.

In java, conditional locks are implemented in the ConditionObject class of AQS. ConditionObject implements the ConditionInterface. Here's an example to get into conditional lock learning.

Use examples

public class ReentrantLockTest {
    public static void main(String[] args) throws InterruptedException {
        // Declare a reentrant lock
        ReentrantLock lock = new ReentrantLock();
        // Declare a conditional lock
        Condition condition = lock.newCondition();

        new Thread(()->{
            try {
                lock.lock();  // 1
                try {
                    System.out.println("before await");  // 2
                    // Waiting conditions
                    condition.await();  // 3
                    System.out.println("after await");  // 10
                } finally {
                    lock.unlock();  // 11
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        
        // Sleep here for 1000ms so that the thread above gets the lock first
        Thread.sleep(1000);
        lock.lock();  // 4
        try {
            // Sleeping 2000 ms here represents the amount of time this thread takes to execute its business
            Thread.sleep(2000);  // 5
            System.out.println("before signal");  // 6
            // Notification condition is established
            condition.signal();  // 7
            System.out.println("after signal");  // 8
        } finally {
            lock.unlock();  // 9
        }
    }
}

The code above is simple, one thread waits for the condition, the other thread notifies that the condition is set, and the number after it represents the order in which the code actually runs, if you can read the basic condition locks in this order, you're almost done.

Source Code Analysis

Main Properties of ConditionObject

public class ConditionObject implements Condition, java.io.Serializable {
    /** First node of condition queue. */
    private transient Node firstWaiter;
    /** Last node of condition queue. */
    private transient Node lastWaiter;
}

You can see that a queue is also maintained in a conditional lock. To distinguish it from an AQS queue, I call it a conditional queue here. First Waiter is the head node of the queue, and lastWaiter is the end node of the queue. What do they do?Then look.

lock.newCondition() method

Create a new conditional lock.

// ReentrantLock.newCondition()
public Condition newCondition() {
    return sync.newCondition();
}
// ReentrantLock.Sync.newCondition()
final ConditionObject newCondition() {
    return new ConditionObject();
}
// AbstractQueuedSynchronizer.ConditionObject.ConditionObject()
public ConditionObject() { }

Create a new conditional lock and finally instantiate the conditional lock by calling the ConditionObject class in AQS.

condition.await() method

The condition.await() method, which indicates that you are waiting for a condition to appear.

// AbstractQueuedSynchronizer.ConditionObject.await()
public final void await() throws InterruptedException {
    // Throw an exception if the thread is interrupted
    if (Thread.interrupted())
        throw new InterruptedException();
    // Add a node to Condition's queue and return it
    Node node = addConditionWaiter();
    // Fully release locks acquired by the current thread
    // Since locks are reentrant, here you have to release all locks you acquire
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // Is it in the synchronization queue
    while (!isOnSyncQueue(node)) {
        // Blocking current thread
        LockSupport.park(this);
        
        // The upper part is to release the lock you own when you call await(), and block your own wait conditions
        // ******************************* Divide Line******************************* //
        // The following part is that the condition has already appeared, trying to acquire the lock
        
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    
    // Try to acquire the lock, note the second parameter, which is the method analyzed in the previous chapter
    // If you don't get it, it will be blocked again (this method is not posted here, interestingly flip over the contents of the previous chapter)
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    // Clear Cancelled Nodes
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    // Thread interrupt correlation
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}
// AbstractQueuedSynchronizer.ConditionObject.addConditionWaiter
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If the end node of the conditional queue is cancelled, clear all canceled nodes from the start node
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        // Reacquire Tail Node
        t = lastWaiter;
    }
    // Create a new node whose wait state is CONDITION
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    // If the tail node is empty, assign the new node to the header node (equivalent to initializing the queue)
    // Otherwise assign the new node to the nextWaiter pointer of the tail node
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    // End Node Points to New Node
    lastWaiter = node;
    // Return New Node
    return node;
}
// AbstractQueuedSynchronizer.fullyRelease
final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        // Get the value of the state variable, repeat the lock, and the value will always add up
        // So this value also represents the number of times locks were acquired
        int savedState = getState();
        // Release all acquired locks at once
        if (release(savedState)) {
            failed = false;
            // Returns the number of times a lock was acquired
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}
// AbstractQueuedSynchronizer.isOnSyncQueue
final boolean isOnSyncQueue(Node node) {
    // Returns false if the wait state is CONDITION or the previous pointer is empty
    // Description has not been moved to the AQS queue
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    // If the next pointer has a value, it indicates that it has been moved to the AQS queue
    if (node.next != null) // If has successor, it must be on queue
        return true;
    // Look forward from the end of AQS to see if the current node can be found, and find also indicates that you are in the AQS queue
    return findNodeFromTail(node);
}

Here are a few difficult points to understand:

(1) Condition's queue is not exactly the same as AQS's;

The queue head node of AQS has no value and is a virtual node.

Condition's queue header node stores real element values and is a real node.

(2) Variations in waitStatus;

First, in the conditional queue, the initial wait state for a new node is CONDITION (-2);

Second, the wait state changes to 0 when moving to the AQS queue (the initial wait state of the AQS queue node is 0);

Then, if blocking is required in the queue of AQS, the wait state of its previous node is set to SIGNAL (-1);

Finally, whether in the Condition queue or the AQS queue, the wait state of canceled nodes is set to CANCELLED(1);

In addition, we will talk later about another wait state called PROPAGATE (-3) when sharing locks.

(3) similar names;

The next node in AQS is next, and the previous node is prev;

The next node in Condition is nextWaiter, and there is no previous node.

If you understand these points, it is still easier and more pleasant to understand the code above. If you do not understand them, Tong Gong pointed out here, I hope you can look back at the code above.

The following summarizes the general process of the await() method:

(1) Create a new node to join the conditional queue;

(2) Fully release the lock held by the current thread;

(3) Block the current thread and wait for conditions to appear;

(4) The condition has already appeared (at this time the node has moved to the AQS queue) and tries to acquire the lock;

That is, inside the await() method is the process of first releasing the lock - > waiting conditions - > acquiring the lock again.

condition.signal() method

The condition.signal() method notification condition has already occurred.

// AbstractQueuedSynchronizer.ConditionObject.signal
public final void signal() {
    // Calling this method throws an exception if the current thread is not holding the lock
    // Indicates that signal() is also executed after acquiring a lock
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // Head node of conditional queue
    Node first = firstWaiter;
    // If a node has a wait condition, notify it that the condition is established
    if (first != null)
        doSignal(first);
}
// AbstractQueuedSynchronizer.ConditionObject.doSignal
private void doSignal(Node first) {
    do {
        // Move back to the first node of the conditional queue
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        // Equivalent to queuing a head node
        first.nextWaiter = null;
        // Transfer node to AQS queue
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}
// AbstractQueuedSynchronizer.transferForSignal
final boolean transferForSignal(Node node) {
    // Change the state of the node to 0, which means it is about to move to the AQS queue
    // If it fails, the node has been changed to canceled state
    // Return false, and the loop above tells you that the next available node will be found
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // Invoke the AQS enqueue method to move nodes to the AQS queue
    // Note that the return value of enq() here is the last node of the node, the old tail node
    Node p = enq(node);
    // Waiting state of previous node
    int ws = p.waitStatus;
    // If the previous node has been cancelled, or the update status to SIGNAL has failed (which also means the previous node has been cancelled)
    // Then wake up the thread corresponding to the current node directly
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    // If updating the last node's wait state to SIGNAL succeeded
    // Returns true, then the loop above is not established, exiting the loop, that is, only one node is notified
    // The current node is still blocked at this time
    // That is, when signal() is called, it does not really wake up a node
    // Just move the node from the conditional queue to the AQS queue
    return true;
}

The general flow of the signal() method is:

(1) Find a non-cancelling node from the head of the conditional queue;

(2) Move it from the conditional queue to the AQS queue;

(3) and move only one node;

Note that calling the signal() method here does not really wake up a node, so when does it wake up?

Remember the first example?Looking back, after the signal() method, the lock.unlock() method will eventually be executed, and then a node will actually wake up, which will continue to execute the code under the await() method's "dividing line" if it was a conditional node.

It's over. Take a closer look at ^^

If you have to use a graph to represent it, I think the following figure can roughly represent it (here is a time series picture, but it can't actually be counted as a real time series chart, just know it):

summary

(1) Re-entry locks refer to locks that can be acquired repeatedly, i.e. automatically when a thread acquires a lock and then attempts to acquire it again;

(2) Reentry locks in ReentrantLock are achieved by continuously accumulating the values of the state variable;

(3) ReentrantLock should be released to match acquisition, that is, several acquisitions and several releases;

(4) ReentrantLock defaults to unfair mode because unfair mode is more efficient;

(5) Conditional lock refers to a lock used to wait for a condition to appear;

(6) The classic use scenario of conditional locks is when a queue is empty and blocked on conditional notEmpty;

(7) Conditional locks in ReentrantLock are implemented through AQS ConditionObject internal classes;

(8) both await() and signal() methods must be used before the lock is released after it has been acquired;

(9) The await() method creates a new node and puts it in the conditional queue, releases the lock completely, then blocks the current thread and waits for the condition to appear;

(10) The signal() method looks for the first available node in the conditional queue to move to the AQS queue;

(11) The unlock() method is called on the thread calling the signal() method to really wake up the node blocked by the condition (at this point the node is already in the AQS queue);

(12) The node then tries to acquire the lock again, and the following logic is basically consistent with that of lock().

Eggs

Why does java have its own keyword synchronized and need to implement a ReentrantLock?

First, they are re-lockable;

Secondly, they all default to unfair models;

Then,..., uh, let's move on to ReentrantLock VS synchronized in the next chapter.

Recommended reading

  1. ReentrantLock Source Code Resolution for Dead-end java Synchronization Series (1) - Fair Lock, Unfair Lock

  2. Beginning of AQS in Dead Javaa Synchronization Series

  3. Write a lock Lock yourself in Dead Javaa Synchronization Series

  4. Unsafe Resolution of Dead java Magic

  5. JMM (Java Memory Model) of Dead Javaa Synchronization Series

  6. volatile analysis of dead-end java synchronization series

  7. synchronized parsing of dead-end java synchronization series

Welcome to pay attention to my public number "Tong Gong Read Source". Check out more articles about source series and enjoy the sea of source code with Tong Gong.

Posted by Walle on Sun, 02 Jun 2019 10:14:31 -0700