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
-
ReentrantLock — Fair lock, unfair lock
-
AQS is the beginning of the Java Sync series
-
Deadknock Java Synchronization series write a Lock Lock yourself
-
Unsafe parsing of Java magic classes
-
JMM (Java Memory Model)
-
Volatile parsing of the Java Synchronization series
-
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.