The problem

(1) What is conditional lock?

(2) What scenarios does conditional lock apply to?

(3) Is the await() of the conditional lock waked when the other thread signal()?

Introduction to the

Conditional lock refers to a lock that is used when the current service scenario cannot be processed by the user after obtaining the lock and the user needs to wait for a certain condition to continue processing.

For example, in a blocking queue, it is impossible to eject an element when there is no element in the queue. In this case, the notEmpty condition must be blocked, and the notEmpty condition can be awakened after another thread has added an element to the queue.

Note that the condition must wait after the lock is acquired, corresponding to the conditional lock of ReentrantLock, which is called after the lock is acquired.

ConditionObject ConditionObject ConditionObject ConditionObject ConditionObject ConditionObject ConditionObject ConditionObject ConditionObject ConditionObject ConditionObject ConditionObject

Use the sample

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
                    // Wait for conditions
                    condition.await();  / / 3
                    System.out.println("after await");  / / 10
                } finally {
                    lock.unlock();  / / 11}}catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        
        // This is set to 1000ms so that the upper line can get the lock first
        Thread.sleep(1000);
        lock.lock();  / / 4
        try {
            // 2000ms represents the time required for this thread to execute business
            Thread.sleep(2000);  / / 5
            System.out.println("before signal");  / / 6
            // The notification conditions are confirmed
            condition.signal();  / / 7
            System.out.println("after signal");  / / 8
        } finally {
            lock.unlock();  / / 9}}}Copy the code

The code above is very simple, one thread waits for the condition, the other notifies the condition is true, and the numbers behind represent the order in which the code actually runs, if you can understand the order in which the basic condition lock is pretty much mastered.

Source code analysis

The 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;
}
Copy the code

The firstWaiter is the head of the queue and the lastWaiter is the tail of the queue. What do they do? Then look at.

The lock. NewCondition () method

Create a new conditional lock.

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

ConditionObject (ConditionObject); ConditionObject (ConditionObject);

Condition. Await () method

The condition.await() method, indicating that you are now waiting for the condition to appear.

// AbstractQueuedSynchronizer.ConditionObject.await()
public final void await(a) throws InterruptedException {
    // If the thread breaks, throw an exception
    if (Thread.interrupted())
        throw new InterruptedException();
    // Add a node to the Condition's queue and return the node
    Node node = addConditionWaiter();
    // Release the lock from the current thread
    // Since the lock is reentrant, we need to release all acquired locks
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // Whether the queue is synchronized
    while(! isOnSyncQueue(node)) {// Block the current thread
        LockSupport.park(this);
        
        Call await() to release the lock and block the condition
        / / * * * * * * * * * * * * * * * * * * * * * * * * * line * * * * * * * * * * * * * * * * * * * * * * * * * / /
        // The following part is the condition has appeared, try to obtain the lock
        
        if((interruptMode = checkInterruptWhileWaiting(node)) ! =0)
            break;
    }
    
    // Try to acquire the lock. Note the second argument, which is the method analyzed in the previous chapter
    // This method will block again if you don't get it.
    if(acquireQueued(node, savedState) && interruptMode ! = THROW_IE) interruptMode = REINTERRUPT;// Clear the cancelled node
    if(node.nextWaiter ! =null) // clean up if cancelled
        unlinkCancelledWaiters();
    // Thread interrupt related
    if(interruptMode ! =0)
        reportInterruptAfterWait(interruptMode);
}
// AbstractQueuedSynchronizer.ConditionObject.addConditionWaiter
private Node addConditionWaiter(a) {
    Node t = lastWaiter;
    // If the tail node of the conditional queue has been cancelled, all cancelled nodes are cleared from the head node
    if(t ! =null&& t.waitStatus ! = Node.CONDITION) { unlinkCancelledWaiters();// Retrieve the 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, the new node is assigned to the head node (equivalent to initializing the queue)
    // Otherwise, the new node is assigned to the nextWaiter pointer of the last node
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    // The tail node points to the new node
    lastWaiter = node;
    // Returns the new node
    return node;
}
// AbstractQueuedSynchronizer.fullyRelease
final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        // Get the value of the state variable, repeatedly get the lock, the value will accumulate
        // So this value also represents the number of times the lock was acquired
        int savedState = getState();
        // Release all acquired locks at once
        if (release(savedState)) {
            failed = false;
            // Returns the number of times the lock was acquired
            return savedState;
        } else {
            throw newIllegalMonitorStateException(); }}finally {
        if(failed) node.waitStatus = Node.CANCELLED; }}// AbstractQueuedSynchronizer.isOnSyncQueue
final boolean isOnSyncQueue(Node node) {
    // If the wait state is CONDITION, or the previous pointer is null, return false
    // Indicates that the AQS queue has not been moved
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    // If the next pointer has a value, the AQS queue has been moved
    if(node.next ! =null) // If has successor, it must be on queue
        return true;
    // Start from the last node of AQS to see if the current node can be found. If the current node is found, it is already in the AQS queue
    return findNodeFromTail(node);
}
Copy the code

Here are a few tricky points:

(1) The queue of Condition is not exactly the same as that of AQS;

The queue head node of AQS does not have any value and is a virtual node. The Condition's queue head node, which stores the actual element values, is a real node.Copy the code

(2) Changes of various waitStatus;

Firstly, in the conditional queue, the initial wait state of the new node is CONDITION (-2). Secondly, the waiting state will change to 0 when moving to the queue of AQS (the initial waiting state of AQS queue node is 0). Then, if the AQS queue needs to block, the wait state of its last node is set to SIGNAL (-1). Finally, the wait state of CANCELLED nodes is set to CANCELLED (1) in both Condition and AQS queues. In addition, we will talk about another kind of wait state called PROPAGATE (-3) later when we are sharing locks.Copy the code

(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.Copy the code

If you understand these points, it is still easy and pleasant to understand the above code. If not, Tong Brother pointed it out here, I hope you go back and look at the above code.

To summarize the general flow 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 the condition to appear;

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

Inside the await() method is the process of releasing the lock first -> waiting for the condition -> getting the lock again.

Condition. The signal () method

The conditional.signal () method notifies that a condition has occurred.

// AbstractQueuedSynchronizer.ConditionObject.signal
public final void signal(a) {
    Call this method to throw an exception if the current thread does not hold the lock
    Signal () is also executed after the lock is acquired
    if(! isHeldExclusively())throw new IllegalMonitorStateException();
    // The head node of the conditional queue
    Node first = firstWaiter;
    // If there is a node waiting for a condition, notify it that the condition is true
    if(first ! =null)
        doSignal(first);
}
// AbstractQueuedSynchronizer.ConditionObject.doSignal
private void doSignal(Node first) {
    do {
        // Move to one place behind the head node of the conditional queue
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        // The head node is removed from the queue
        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, that is, to move to the AQS queue
    // If it fails, the node has been changed to cancel
    // Return false to find the next available node through the loop above
    if(! compareAndSetWaitStatus(node, Node.CONDITION,0))
        return false;

    // Call AQS 'enqueue method to move the node to AQS' queue
    // Note that the return value of enq() is the last node of node
    Node p = enq(node);
    // The wait state of the last node
    int ws = p.waitStatus;
    // If the last node is cancelled, or the update status to SIGNAL fails (also indicating that the last node is cancelled)
    // Wake up the thread corresponding to the current node
    if (ws > 0| |! compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread);// Update the wait status of the last node to SIGNAL successfully
    // returns true, the loop is broken, and only one node is notified
    // The current node is still blocked
    Signal () does not actually wake up a node
    // Just move the node from conditional queue to AQS queue
    return true;
}
Copy the code

The signal() method looks like this:

(1) Start from the head node of the conditional queue to find a non-cancelled node;

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

(3) Only one node is moved;

Note that calling signal() does not actually wake up a node, but when?

Remember the beginning example? Looking back, the signal() method will eventually execute lock.unlock(), which will actually wake up a node, and the awakened node will continue to execute the code below the “boundary” of the await() method if it was once a conditional node.

That’s it. Take a look ^^

If you had to use a diagram, I think the following diagram can be roughly represented (this is a sequence diagram, but it is not really a sequence diagram, just understand) :

conclusion

(1) Reentrant lock refers to a repeatable lock, that is, when a thread tries to acquire the lock again after acquiring it, it will automatically acquire the lock;

(2) In ReentrantLock, ReentrantLock is achieved by accumulating the value of the state variable;

(3) The release of ReentrantLock must match the capture, that is, the release of several times;

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

(5) Conditional lock refers to the use of a lock in order to wait for a condition;

(6) The classic use scenario of conditional locking is that the queue is blocked on condition notEmpty when the queue is empty;

(7) The conditional lock in ReentrantLock is implemented by the inner class ConditionObject of AQS.

(8) Both await() and signal() methods must be used before releasing the lock after acquiring it;

The (9) await() method will create a new node and put it in the condition queue, then release the lock completely, then block the current thread and wait for the condition to appear;

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

(11) Calling unlock() in the thread that calls signal() actually wakes up the node blocking on the condition (the node is already in the AQS queue);

After (12), the node attempts to acquire the lock again, and the logic behind it is basically the same as that of lock().

eggs

Why implement a ReentrantLock when Java has its own synchronized keyword?

First, they are both reentrant locks;

Second, they all default to unfair modes;

And then,… Well, we’ll dig deeper into ReentrantLock VS synchronized in the next chapter.

Recommended reading

  1. ReentrantLock — Fair lock, unfair lock

  2. AQS is the beginning of the Java Sync series

  3. Deadknock Java Synchronization series write a Lock Lock yourself

  4. Unsafe parsing of Java magic classes

  5. JMM (Java Memory Model)

  6. Volatile parsing of the Java Synchronization series

  7. Synchronized parsing of the Java synchronization series


Welcome to pay attention to my public number “Tong Elder brother read source code”, view more source code series articles, with Tong elder brother tour the ocean of source code.