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
-
Write a lock Lock yourself in Dead Javaa Synchronization Series
-
JMM (Java Memory Model) of Dead Javaa Synchronization Series
-
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.