Statement
This article was first published under Public No. cxuan, a programmer who contributed his own article.
I've communicated with cxuan, posted a public number, and posted my own blog to mark it as an original.
This article has been a long time with many pictures. I hope you like it.
In addition, interested little partners can focus on the Personal Public Number: A flower is not romantic
The Public Number is just starting to operate and wants to grow with you.
Preface
When it comes to concurrency, we have to say that AQS(AbstractQueuedSynchronizer), which is an abstract queue synchronizer, defines many lock-related methods internally. The well-known ReentrantLock, ReentrantReadWriteLock, CountDownLatch, Semaphore, and so on, are all based on AQS.
Let's first look at the UML diagrams associated with AQS:
Mind mapping:
AQS Implementation Principle
A volatile int state (representing a shared resource) and a FIFO thread are maintained in AQS waiting for a queue to enter when multithreaded competing resources are blocked.
volatile ensures visibility under multiple threads. When state=1 indicates that the current object lock has been occupied, other threads fail to lock. Threads that fail to lock are placed in a FIFO wait queue, the biqueue is suspended by the UNSAFE.park() operation, and other threads that acquire the lock are awakened to release the lock.
In addition, the operation of state is to ensure the security of concurrent modifications through CAS.
The exact principle can be summarized in a diagram:
There are many ways to implement locks in AQS.
- getState(): Gets the flag state value of the lock
- setState(): Sets the flag state value for the lock
- tryAcquire(int): Acquire locks exclusively.Attempts to acquire resources return true for success and false for failure.
- tryRelease(int): Release the lock exclusively.Attempts to free resources return true for success and false for failure.
There are still some methods that are not listed here. Next, we will use ReentrantLock as a breakthrough point to step through the internal implementation of AQS in the form of source code and drawing.
directory structure
This article is ready to simulate the scenario of multithreaded competing locks and releasing locks to analyze AQS source code:
Three threads (thread one, thread two, thread three) lock/release simultaneously
The catalog is as follows:
- Internal implementation of AQS when thread one lock succeeds
- Data Model of AQS Waiting Queue on Thread Two/Three Lock Failure
- Principle of Thread-1 Release Lock and Thread-2 Acquire Lock
- Explain how fair locks work in threading scenarios
- Explain how await() and signal() are implemented in Condition through a thread scenario
Here, the data structure and implementation principle of AQS after each thread locks and unlocks are analyzed by drawing.
Scene Analysis
Thread lock succeeded
If there are three threads concurrently preempting the lock, then thread one succeeds and thread two and thread three fail to preempt the lock. The specific execution procedure is as follows:
At this point, the AQS internal data is:
Thread 2, Thread 3 failed to lock:
You can see from the diagram that Node in the waiting queue is a two-way Chain table, where SIGNAL is the waitStatus attribute in Node and there is also a nextWaiter attribute in Node, which is not illustrated in the diagram and will be explained later in Condition.
Specifically, look at the implementation of the preemptive lock code:
java.util.concurrent.locks.ReentrantLock .NonfairSync:
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
The ReentrantLock unfair lock used here is where threads come in and try to preempt the lock directly using CAS, if the preemption success state value is changed back to 1, and the object exclusive lock thread is set to the current thread.As follows:
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
Thread 2 failed to preempt lock
We analyze the real-world scenario that once a thread successfully preempts a lock, the state changes to 1, and thread 2 will inevitably fail to modify the state variable through CAS.At this point, the data in the FIFO(First In First Out First In First Out) queue in AQS is shown as follows:
Let's look at the logic of Thread 2 execution step by step:
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire():
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
First look at the implementation of tryAcquire():
java.util.concurrent.locks.ReentrantLock .nonfairTryAcquire():
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
The nonfairTryAcquire() method first gets the value of the state. If it is not 0, the lock on the current object is already occupied by another thread. Then it determines if the thread holding the lock is the current thread. If it is, it adds up the state value, which is the specific implementation of the reentrant lock. When the lock is released, it decreases the state value in turn.
If the state is 0, a CAS operation is performed to attempt to update the state value to 1, and if the update succeeds, the current thread locks successfully.
Take Thread 2 for example, because Thread 1 has modified the state to 1, it will not be successful for Thread 2 to modify the value of the state through CAS.Locking failed.
Thread 2 executes tryAcquire() and returns false, followed by addWaiter(Node.EXCLUSIVE) logic to join itself to a FIFO wait queue, as follows:
java.util.concurrent.locks.AbstractQueuedSynchronizer.addWaiter():
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
This code first creates a Node node bound to the current thread, which is a two-way Chain table.At this point, the tail pointer inside the wait pair is empty, and the enq(node) method is called directly to join the current thread to the end of the wait queue:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
The tail pointer is empty on the first pass of the loop, enters if logic, sets the header pointer using the CAS operation, and points the header to a newly created Node node.Data in AQS at this time:
After execution, head, tail, t all point to the first Node element.
Next, you execute the second loop and enter the else logic, where you already have the head node, where you suspend the corresponding Node node of thread two behind the head node.There are two Node nodes in the queue:
When the addWaiter() method finishes executing, it returns the node information created by the current thread.Continue to acquireQueued(addWaiter(Node.EXCLUSIVE), arg) later
Logically, the parameter passed in at this time is the Node node information corresponding to Thread 2:
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued():
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndChecknIterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
The acquireQueued() method first determines if the corresponding preceding node of the currently incoming Node is a head and, if so, attempts to lock it.If the lock succeeds, the current node is set as the head node, and the previous head node is emptied for subsequent garbage collection.
If the lock fails or the front node of Node is not the head node, the shouldParkAfterFailedAcquire method is passed
Change the waitStatus of the head node to SIGNAL=-1, and finally execute the parkAndChecknIterrupt method, calling LockSupport.park() to suspend the current thread.
At this point, the data in AQS is as follows:
Thread 2 is now in the AQS waiting queue, waiting for other threads to release the lock to wake it up.
Thread 3 failed to preempt lock
After reviewing the analysis of thread two's failure to preempt a lock, it's easy to analyze thread three's failure to preempt a lock. First, look at the addWaiter(Node mode) method:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
At this point, wait for the tail node of the queue to point to thread two, and after entering if logic, point the tail node back to thread three through the CAS directive.Thread three then calls the enq() method to perform the queue operation, which is consistent with thread two above. After queuing, it modifies waitStatus=SIGNAL in the corresponding Node for thread two.Finally, thread three is suspended.The data waiting for the queue at this time is shown in the following figure:
Thread 1 release lock
Now let's analyze the process of unlocking the lock. First, as soon as the thread releases the lock, it will wake up the rear node of the head node, which is now thread 2. The detailed procedure is as follows:
Waiting for queue data after execution is as follows:
Thread 2 is now awakened and continues to attempt to acquire the lock. If the acquisition fails, it will continue to be suspended.If the lock is acquired successfully, the data in AQS is shown in the figure:
Next, step by step, let's look at the code that releases the lock as soon as the thread releases it:
java.util.concurrent.locks.AbstractQueuedSynchronizer.release()
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
The first step is to execute the tryRelease() method, which is implemented in ReentrantLock. If tryRelease succeeds, it continues to determine if the waitStatues of the head node are zero. As we have seen before, the waitStatue of the head is SIGNAL(-1), where the unparkSuccessor() method is executed to wake up the back node of the head, which corresponds to Thread 2 in the figure above.Node node.
Look at the implementation in ReentrantLock.tryRelease():
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
After executing ReentrantLock.tryRelease(), the state is set to 0 and the exclusive lock on the Lock object is set to null.Now look at the data in AQS:
Then execute the java.util.concurrent.locks.AbstractQueuedSynchronizer.unparkSuccessor() method to wake up the back node of the head:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
The main thing here is to set the waitStatus of the head node to zero, then undirect the head node next, leaving the head node empty and waiting for garbage collection.
At this point, the head pointer is redirected to the corresponding Node node of thread two, and the LockSupport.unpark method is used to wake up thread two.
The awakened thread 2 then attempts to acquire the lock, modifying the state data with the CAS directive.
After execution, you can view the data in AQS:
At this point, thread 2 is awakened, thread 2 continues execution where it was previously park ed, and the acquireQueued() method continues.
Thread 2 Wake Up Continue Locking
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
Thread 2 is awakened at this point, and continues with the for loop to determine if the preceding node of thread 2 is head, and if it continues to use the tryAcquire() method to attempt to acquire a lock, it is actually using the CAS operation to modify the state value, and if it is successful, it means that the lock was acquired.Thread 2 is then set as the head node, and the previous head node data is emptied, and the empty node data is waiting to be garbage collected.
Thread 3 succeeds in acquiring the lock and the queue data in AQS is as follows:
Waiting for the data in the queue to be garbage collected.
Thread 2 release lock / Thread 3 lock
When Thread 2 releases the lock, it wakes up the suspended thread 3, which has the same process as above. The awakened thread 3 tries to lock again. The code can refer to the above.The detailed flowchart is as follows:
In this case, the queue data in AQS is shown as follows:
Fair Lock Implementation Principle
All of the above locking scenarios are based on unfair locks, which are the default implementation of ReentrantLock. Let's look at how fair locks work. Here's a diagram to explain the difference between fair locks and unfair locks:
Unfair Lock Execution Process:
Here's another example of using the previous thread model: when thread two releases the lock, wake up the suspended thread three, thread three executes the tryAcquire() method, uses the CAS operation to attempt to modify the state value, and if another thread four comes in to perform the lock operation, it also executes the tryAcquire() method.
This creates competition, and if Thread 4 succeeds in acquiring the lock, Thread 3 still needs to be suspended in the waiting queue.This is known as an unfair lock, where threads work hard in queues until they get the lock themselves, only to see the threads queue up to get the lock.
Fair Lock Execution Process:
When a fair lock is locked, it is determined that there are nodes in the AQS waiting queue, and if there are nodes, they will be queued directly. The code is as follows.
A fair lock implements the tryAcquire() method separately when it acquires a lock.
#java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire():
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
The tryAcquire() method for fair locks in ReentrantLock is executed here
#java.util.concurrent.locks.ReentrantLock.FairSync.tryAcquire():
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
If the value of state is not 0 and the thread that acquired the lock is not the current thread, false is returned directly to indicate that the acquisition of the lock failed and was joined the waiting queue.If it is the current thread, it can reentrant to acquire the lock.
If state=0 means that no threads hold locks at this time, execute hasQueuedPredecessors() to determine if any elements exist in the AQS wait queue, and if there are other wait threads, it will also join the end of the wait queue, so that it truly comes first, comes first, and locks sequentially.The code is as follows:
#java.util.concurrent.locks.AbstractQueuedSynchronizer.hasQueuedPredecessors():
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
Interestingly, this code returns false to indicate that there are no nodes in the queue or that only one node is a node created by the current thread.Returning true means there are waiting nodes in the queue, and the current thread needs to be queued.
First determine if the head equals tail, if there is only one Node in the queue, then the head equals tail, and then the head's post-node, which is definitely null. If the thread corresponding to this Node is the same thread as the current thread, then false is returned, indicating that there is no waiting node or that the waiting node is the ode node created by the current thread.The current thread will attempt to acquire a lock at this point.
If the head and tail are not equal, a node in the queue waiting for a thread to create returns true directly. If there is only one node and the thread of this node is inconsistent with the current thread, it also returns true.
The difference between an unfair lock and a fair lock:
Unfair locks perform better than fair locks.Unfair locks can reduce CPU wake-up threads overhead, increase overall throughput efficiency, and reduce the number of wake-up threads without requiring the CPU to wake all threads
Unfair locks perform better than fair locks, but can cause thread hunger.In the worst case, there may be a thread that has never acquired a lock.However, compared to performance, hunger can be ignored temporarily, which may be one reason why ReentrantLock creates unfair locks by default.
Condition implementation principles
Introduction to Condition
The core functionality provided by AQS is described above, but of course it has many other features, so let's continue with Condition.
Conditions emerged in java 1.5 as an alternative to traditional Object wait(), notify() for interthread collaboration, which is safer and more efficient than using Object wait(), notify(), await(), signal() in Conditions.Consequently, Conditions are generally recommended.
The method in Condition is implemented in AbstractQueueSynchronizer, which mainly provides awaite(Object.wait()) and signal(Object.notify()) calls to the outside world.
Condition Demo example
Using sample code:
/**
* ReentrantLock Implement Source Learning
* @author A flower is not romantic
* @date 2020/4/28 7:20
*/
public class ReentrantLockDemo {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
System.out.println("Thread lock succeeded");
System.out.println("Thread 1 Execution await Suspended");
condition.await();
System.out.println("Thread wakes up successfully");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("Thread released lock successfully");
}
}).start();
new Thread(() -> {
lock.lock();
try {
System.out.println("Thread 2 lock succeeded");
condition.signal();
System.out.println("Thread 2 Wakes Thread 1");
} finally {
lock.unlock();
System.out.println("Thread 2 released lock successfully");
}
}).start();
}
}
The results are as follows:
Here, the thread first acquires the lock, then uses the await() method to suspend the current thread and release the lock, and thread two acquires the lock and wakes thread one with a signal.
Condition implementation schematic diagram
We also use the demo above as an example to perform the following process:
Thread 1 executes the await() method:
First look at the specific code implementation, #java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject.await():
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
The await() method first calls addConditionWaiter() to join the current thread to the Condition queue.
After execution we can look at the data in the Condition queue:
The implementation code is:
private Node addConditionWaiter() {
Node t = lastWaiter;
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
A Node node is created with the current thread and waitStatus is CONDITION.The node's lock is then released, the previously resolved release() method is called, and when the lock is released, the suspended thread two wakes up, and thread two continues to attempt to acquire the lock.
The isOnSyncQueue() method is then called to determine whether the current node is the head node in the Condition queue or, if so, to suspend the current thread in Condition by calling LockSupport.park(this).Thread 2 acquired the lock successfully as soon as the thread was suspended.
The specific process is as follows:
Thread 2 executes the signal() method:
First, let's consider that Thread 2 has acquired the lock, and there is no more data in the AQS wait queue.
Next, let's look at how thread two wakes up thread one:
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
Determine if the current thread is the one that acquires the lock, or throw an exception if it is not.
The doSignal() method is then called to wake up the thread.
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;
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
From the transferForSignal() method, we know from the analysis above that there is only one Node node created by thread one in the Condition queue, and waitStatue is CONDITION. First, modify the current node waitStatus to 0 by CAS, then execute enq() method to join the current thread to the waiting queue and return to the preceding node of the current thread.
The code to join the waiting queue is also analyzed above, and the data in the waiting queue is as follows:
Then start modifying the current node's preceding node waitStatus to SIGNAL through CAS, and wake up the current thread.At this point, the AQS waiting queue data is:
As soon as the thread wakes up, continue executing the while loop in the await() method.
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
Executing the isOnSyncQueue() method returns false because the waitStatus for thread one has been modified to zero at this time.Jump out of the while loop.
Next, the acquireQueued() method is executed, as mentioned earlier, and an attempt to retrieve the lock continues to be suspended if the lock acquisition fails.Not until another thread releases the lock.
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
At this point, the process of thread one has been analyzed. After thread two releases the lock, the thread will continue retrying to acquire the lock, and the process will end.
Condition Summary
We summarize the comparison between Condition and wait/notify:
-
Condition s can precisely control many different conditions, wait/notify can only be used with the synchronized keyword, and can only wake up one or all of the waiting queues;
-
Conditions need to be controlled using Lock. When using Lock, be aware of the timely unlock() after lock(). Conditions have a mechanism similar to await, so there will be no deadlock caused by locking, and the underlying mechanism is park/unpark, so there will be no deadlock caused by waking up and hanging. In a word, there will be no deadlock, but wait/notify will occurWake up before hanging deadlock.
summary
This shows how and how ReentrantLock is implemented by combining three threads locking/releasing locks one by one. The underlying layer of ReentrantLock is based on AQS, so we also have a deep understanding of AQS.
In addition, the implementation principles of fair lock and unfair lock are also introduced. The implementation principles of Condition are basically explained by using source code + drawing to make it easier for everyone to understand.
Reference material:
- Bringing Java to the Supervisor's Vessel--the cornerstone of concurrent data structure
https://juejin.im/post/5c11d6376fb9a049e82b6253
- Java Concurrent AQS Details https://www.cnblogs.com/waterystone/p/4920797.html