In-depth analysis of AQS principle based on ReentrantLock
This article is based on JDK8. The source code implementation is slightly different in JDK9 and later versions, but the idea is the same, with a new type of VarHandle in JDK9 that replaces most of the Unsafe features.
Most synchronization classes in Java (ReentrantLock, CountDownLatch, Semaphore, ReentrantReadWriteLock, etc.) are implemented based on AQS. AQS provides a simple framework for atomically managing synchronization state (state), blocking and waking up threads, and implementing a FIFO two-ended queue model. AQS internal queue is a variant of CLH queue. This article starts from ReentrantLock, analyzes the lock and unlock process, in the ReentrantLock scenario, the use of AQS and source code analysis. This paper is only an introduction to understand the synchronization tools and AQS principles. Based on these principles, I can further study the application of AQS in other synchronization classes.
1 ReentrantLock
1.1 already features
Already and Synchronized lock
What they have in common:
- Both are reentrant locks, and the same thread can acquire the same lock more than once.
- Both guarantee visibility and mutual exclusion.
- Are used to coordinate multi-thread access control of shared objects and variables.
Differences between the two:
- ReentrantLock requires manual locking and unlocking, and synchronized automatically releases the lock.
- ReentrantLock supports fair locking, whereas synchronized does not.
- ReentrantLock allows you to attempt to obtain a lock and set the timeout period for obtaining the lock. It also allows you to respond to interruptions during the lock acquisition process, making it more flexible to use. Synchronized cannot be interrupted while waiting to acquire a lock.
- ReentrantLock is API level and synchronized is JVM level.
- Lock is the interface and synchronized is the keyword.
- ReentrantLock can bind multiple Condition queues, whereas synchronized can bind only one.
- The underlying implementation is different. Synchronized is synchronous blocking and uses a pessimistic concurrency strategy. ReentrantLock is synchronous non-blocking and uses an optimistic concurrency strategy.
- Synchronized when the system can automatically release the lock, so it will not put the life and death lock. Lock The Lock must be manually released. If the Lock is not actively released, a deadlock occurs. So the Lock needs to release the Lock in the finally block.
- Lock Use read locks to improve the efficiency of multithreaded read operations.
1.2 Analyze the relationship between ReentrantLock and AQS
ReentrantLock uses AQS synchronization status and blocking queue to realize lock and unlock operations. ReentrantLock is only one of the scenarios of AQS implementation. Many concurrent classes in JUC are implemented based on AQS, and the value of AQS synchronization status in different scenarios represents different meanings. It depends on how synchronous classes are implemented.
The relationship between ReentrantLock and AQS is simple. AQS is like a standards-setting organization, and anyone else who wants to use my functions must implement the standards I have developed. The standard set by AQS is that after the subclass inherits my AQS, it needs to implement the methods I define, such as tryAcquire and tryRelease, etc. (there are many methods in it, which subclass should be implemented according to its own functions). These methods are not called directly by the object of the subclass, but are wrapped internally by AQS itself and used in conjunction with blocking queues. Therefore, the lock method of ReentrantLock class is to call AQS acquire, and AQS Acquire method is to call tryAcquire and join blocking queue, etc. The method of joining queue is the core function of AQS, so it does not need to inherit the subclass to implement it. Using ReentrantLock’s unfair lock as an example, you can see the lock and unlock method call flow below:
Acquire is a template method that internally calls the tryAcquire, addWaiter, and acquireQueued methods. TryAcquire is overridden in a subclass, so tryAcquire is a hook method that implements the subclass.
Here is a diagram of the ReentrantLock unfair lock class:NonfairSync is already inner classes, NonfairSync inherited from the Sync, Sync inherited from AbstractQueuedSynchronizer. From this class diagram, combined with the previous call between methods diagram, the general flow of locking and unlocking should be clear.
Here, the relationship between ReentrantLock and AQS is probably introduced, the following is a detailed introduction to the principle of AQS, in-depth analysis of the source code.
2 AQS principle analysis
2.1 AQS Principle Overview
From the whole to the details, follow the following six-step process to analyze the AQS framework:
2.1.1 Principle overview of AQS
2.1.2 AQS data structure
Before AQS source code, need to understand the underlying data structure of AQS, as long as the master of its data structure, in order to understand the implementation of the source code. Even need to spend a lot of source code, repeated thinking, to understand the author’s intention to write so.
AQS internal data structure — Node, Node is a bidirectional linked list, is a variant of CLH queue, Node Node attributes and methods are introduced below:
The method attributes | explain |
---|---|
waitStatus | Status of the current node in the queue |
prev | Precursor node |
next | The subsequent nodes |
thread | Thread of the current node |
nextWaiter | Points to the next node in the CONDITION state. (Since this article does not cover the CONDITION Queue, this pointer is not covered.) |
predecessor() | Returns the precursor node if a NullPointerException is not thrown |
There are two modes of acquiring locks:
model | meaning |
---|---|
SHARED | Indicates that multiple threads can share the same lock |
EXCLUSIVE | Indicates that the thread acquires the lock exclusively |
The meanings of several enumerated values of waitStatus:
Enumerated values | meaning |
---|---|
0 | The default value when the node is initialized |
SIGNAL | -1, the current thread needs to wake up the successor node to release or cancel the lock |
CANCELLED | 1, the current thread to cancel or interrupt state |
CONDITION | -2 is related to Condition. The node identified by Condition is in the wait queue, and the thread of the node is waiting on Condition. When other threads call Condition’s signal() method, the node in Condition state will be transferred from the wait queue to the synchronization queue, waiting for the synchronization lock. |
PROPAGATE | -3, related to the shared mode, in which this state identifies the thread of the node as runnable. |
2.1.3 AQS Synchronization status
Now that you know the data structure, let’s look at the AQS synchronization State — State. AQS maintains a field named state, which means synchronization state, and is modified by Volatile to show the current lock status of critical resources.
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private volatile int state;
Copy the code
The variable state is the core of AQS, and the specific meaning needs to be decided by the synchronous subclass of AQS. You can manipulate state as follows:
methods | meaning |
---|---|
protected final int getState() | To obtain the state value |
protected final void setState(int newState) | Set the state value |
protected final boolean compareAndSetState(int expect, int update) | Update the state value in CAS mode |
These methods are Final, which means they cannot be overridden in subclasses.
2.1.4 AQS Joins the queue
Through the analysis of ReentrantLock lock lock process, analysis of AQS into the queue source. Through the following source analysis to understand.
2.1.5 AQS Removes queues
Through the analysis of ReentrantLock unlock process, analysis of AQS out of the queue source. Through the following source analysis to understand.
2.1.6 AQS Interrupt mechanism
AQS uses the knowledge of cooperative interrupts. AQS does not handle interrupts and returns the result of the interrupt to the synchronizer’s own implementation. The source code is shown in the acquireQueued parkAndCheckInterrupt method. The thread.interrupted () method checks for an interruption flag (this method returns the interruption status of the current Thread and sets the interruption flag to False). After the interruption, the Thread may continue to acquire the lock and only record the interruption result.
2.2 Source analysis from ReentrantLock to AQS
ReentrantLock unfair locking process:
Analyzing source code must run, and then break points step by step. Through the lock and unlock analysis
public class Lock_01 {
public static void main(String[] args) {
Lock lock = new ReentrantLock();ReentrantLock(true) Fair lock, ReentrantLock(false) Unfair locklock.lock(); lock.unlock(); }}Copy the code
// java.util.concurrent.locks.ReentrantLock
public void lock(a) {
sync.lock();Call NonfairSync's lock method
}
Copy the code
// java.util.concurrent.locks.ReentrantLock#NonfairSync
final void lock(a) {
if (compareAndSetState(0.1))// CAS sets the state value of AQS to 1
setExclusiveOwnerThread(Thread.currentThread());// The thread that acquired the lock is saved
else
acquire(1);// Call AQS acquire method
}
Copy the code
Since the lock is not fair, the state value is directly modified through CAS. If the lock is captured, it does not need to enter the waiting queue. This is why unfair locking is recommended because it is efficient. So why is it efficient?
A: Entering the queue requires blocking and arousing the thread, which involves switching the thread’s thread thread, leading to a system context switch. The time consumed in this process is greater than the time spent holding the lock, so the thread entering the queue wastes resources. In addition, when nodes are added to the tail by CAS, they will spin all the time when the concurrency is large, wasting CPU resources.
CompareAndSetState (0, 1) is cleverly designed to improve performance despite the loss of code cleanliness. Even without the if code, there is no problem calling the Acquire method directly, because the acquire method has duplicate code. There is a similar design in the addWaiter method below.
// java.util.concurrent.locks.AbstractOwnableSynchronizer
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
Copy the code
// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
if(! tryAcquire(arg) &&Nonfairsync.tryacquire (arg). This method is equivalent to the hook method. The superclass defines the method and lets the subclass implement it
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// the AQS tryAcquire method is implemented
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
Copy the code
- TryAcquire method: Call the tryAcquire method of NonfairSync and try to acquire the lock. After the lock is obtained, no subsequent enqueue method is executed.
- AddWaiter method: Adds the current thread to the end of the queue in a safe way.
- AcquireQueued method that attempts to acquire the lock and blocks the current thread.
The above three methods tryAcquire, addWaiter and acquireQueued are AQS core methods. The core method of tryAcquire lock is implemented by the synchronizer itself, so you only need to implement the tryAcquire method to customize the synchronization lock. AddWaiter and acquireQueued are the core methods of AQS, which are transparent and can be ignored by custom synchronization locks.
These three methods (tryAcquire, addWaiter, and acquireQueued) are analyzed in detail.
The tryAcquire method called in the acquire method in AQS is the tryAcquire method of NonfairSync:
// java.util.concurrent.locks.ReentrantLock#Sync#NonfairSync
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);// Call the Sync class's nonfairTryAcquire method
}
Copy the code
// java.util.concurrent.locks.ReentrantLock#Sync
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();// Get the current thread object
int c = getState();// Get the state value in AQS
if (c == 0) {// c==0, indicating that no thread has acquired the lock
if (compareAndSetState(0, acquires)) {// Try to change the state value through CAS
setExclusiveOwnerThread(current);// State changed successfully, save the current thread as an exclusive lock thread
return true;// Return true to obtain the lock successfully}}else if (current == getExclusiveOwnerThread()) {// Check whether the current thread is an exclusive lock thread
int nextc = c + acquires; // Reenter the lock, the number of times to obtain the lock increased by 1
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");// Lock reentrant times exceed the range of int, should be state int
setState(nextc);// Set the state value to save the lock times
return true;// Return true to obtain the lock successfully
}
return false;// Return false, failed to acquire lock
}
Copy the code
// java.util.concurrent.locks.AbstractQueuedSynchronizer
protected final void setState(int newState) {
state = newState;
}
Copy the code
After the tryAcquire method is analyzed, the addWaiter method is analyzed. TryAcquire lock failed to create a Node Node from the current thread and add it to the AQS queue.
// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
if(! tryAcquire(arg) &&// tryAcquire returns false and executes the subsequent method
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
Copy the code
Here’s the addWaiter method:
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);// Create a queue node using the current thread. Node is in exclusive mode
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;// pred performs the current queue tail
if(pred ! =null) { // The queue is not empty
node.prev = pred; // The precursor of the current node points to tail
if (compareAndSetTail(pred, node)) {// Set the current node to tail via CAS
pred.next = node;// tail's successor points to the current node
return node;// The current node is added to the end of the queue
}
}
enq(node);// The node joins the queue
return node;// The current node is returned
}
Copy the code
This method is designed with the same idea as the lock method above, with a loss of code cleanliness and improved performance. If code can not, there is no problem, enQ will have duplicate code.
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private Node enq(final Node node) {
for (;;) {// Infinite loop, through the way of spin, to successfully add nodes to the end of the queue
Node t = tail;// t saves the current team tail pointer
if (t == null) { // The Must initialize queue is empty
if (compareAndSetHead(new Node())) // Initialize head via CAS
tail = head;// tail points to the head node
} else {
node.prev = t;// Next of the current node points to the tail node
if (compareAndSetTail(t, node)) {// Use CAS to set the current node as a tail
t.next = node;// Next of the original tail points to the current node
returnt; Return the original tail}}}}Copy the code
When t==null, an empty node is created as the head node. The head node is empty and does not hold any threads, because the head node is needed to wake up the node thread to continue.
At this point, the current thread is successfully enqueued. Then analyze the acquireQueued method:
// java.util.concurrent.locks.AbstractQueuedSynchronizer
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;// Store thread interrupt status
for (;;) {
final Node p = node.predecessor();// Get the precursor node of node
if (p == head && tryAcquire(arg)) {// If the current node is the successor of the head node, try to acquire the lock. It is possible that the head node has released the lock, but has not yet notified the successor node, so try to acquire the lock
setHead(node);Set the current node as the head node
p.next = null; // help GC
failed = false;
return interrupted;// return thread interrupt status
}
if (shouldParkAfterFailedAcquire(p, node) && // Find the valid (not cancelled) node before the current node, the current node next points to this node, and set waitState=-1 on the precursor node to wake up the current interface
parkAndCheckInterrupt())// Block the thread and return the interrupted state
interrupted = true;// The thread was interrupted while blocking}}finally {
if (failed) // If the thread throws an exception and does not execute properly to this point, the value of failed is true, so the current node is cancelledcancelAcquire(node); }}Copy the code
The main functions of this method are as follows:
- The current node attempts to obtain the lock. If the lock is successfully obtained, the system returns. Obtain failed, perform the following steps;
- Find the effective precursor node of the current node, and change the waitState value of the newly found precursor node to -1.
- Blocks the current thread and waits for the precursor node to wake up.
- If the thread throws an exception (interrupt, etc.) during the above steps, cancel the current node. If the current node is the head node, wake up the successor node.
P ==head, why should I try to get the lock?
A: The need to unlock combination analysis, here said the results first, unlock after releasing the lock (that is, changing the state to 0), there is no time to notice the subsequent nodes, then the current node to try to get the lock, if after successful don’t block the thread, improve performance, reduce the blocking and awakens the waste of resources, because involves a context switch.
Analyze the shouldParkAfterFailedAcquire method:
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/* * If waitStatus=-1, the current node can safely be blocked */
return true;
if (ws > 0) {
/* * If the precursor node is cancelled, skip the precursor node and continue to find */
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;// Find the next of the uncancelled precursor node pointing to the current node
} else {
/* * Change the waitStatus of the current node to -1 */ via CAS
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
Copy the code
This step is simply to find the precursor node and change the waitStatus value of the precursor node to -1 to ensure that the current node can be woken up if it is blocked.
Analyze the parkAndCheckInterrupt method:
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private final boolean parkAndCheckInterrupt(a) {
LockSupport.park(this);// Block the current thread
return Thread.interrupted();// Get whether the thread was interrupted while blocking, return true if interrupted, and change the interrupted status to false. AQS does not handle interrupt operations; it is up to the implementation class to decide how.
}
Copy the code
CancelAcquire method analysis:
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
// Skip the cancelled precursor node
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// Next pointer to the precursor node
Node predNext = pred.next;
// Change the node waitStatus value to -1, which is cancelled
node.waitStatus = Node.CANCELLED;
// If node is a tail, use CAS to set the precursor node to the tail
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);// Next points to null on the precursor node through CAS
} else {
int ws;
if(pred ! = head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <=0&& compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread ! =null) {// The precursor node is not a header, the waitStatus of the precursor node is -1 or can be changed to -1, and the precursor thread is not empty
Node next = node.next;
if(next ! =null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);// Modify the next pointer to the precursor node
} else {
unparkSuccessor(node);// The precursor node is a head node, or the precursor node has been canceled, and the successor node of the current node is awakened to continue execution (but does not necessarily acquire the lock, may continue to block).
}
node.next = node; // help GC}}Copy the code
Analysing the unparkantecedent: it is simpler to go back and find the nearest successor to the current one and wake it up to go ahead.
private void unparkSuccessor(Node node) {
// Try to clear the wait state of the current node. If this fails or the waiting thread changes the state, there is no problem.
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// Check whether the successor node of the current node is valid (not empty and not cancelled)
Node s = node.next;
if (s == null || s.waitStatus > 0) {// If the successor node of the current node is invalid or cancelled, find the node nearest to the current node
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);// Wake up the node thread
}
Copy the code
Think about why you need to look backwards rather than forwards.
Prev = t; tail = t; next=node; Node. prev=t is a successful node, but prev.next=node is not always executed, so it is ok to search from back to front.
Here, the unfair lock lock process is finished analysis, a brief summary: try to obtain the lock, get the lock, successful return. Not getting the lock, joining the queue, blocking the thread.
Unfair lock unlocking process:
public class Lock_01 {
public static void main(String[] args) {
Lock lock = new ReentrantLock();ReentrantLock(true) Fair lock, ReentrantLock(false) Unfair locklock.lock(); lock.unlock(); }}Copy the code
// java.util.concurrent.locks.ReentrantLock
public void unlock(a) {
sync.release(1);// call the parent AQS release method
}
Copy the code
Analyze the release method:
// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final boolean release(int arg) {
if (tryRelease(arg)) {TryRelease, an implementation of the method call subclass
Node h = head;// h points to the current header
if(h ! =null&& h.waitStatus ! =0)// Determine if there are any successor nodes to wake up
unparkSuccessor(h);// Wake up the successor node of the header
return true;// Return true to unlock successfully
}
return false;// Return false, unlock failed
}
Copy the code
Analyze the tryRelease method:
// java.util.concurrent.locks.ReentrantLock#Sync
protected final boolean tryRelease(int releases) {
int c = getState() - releases;// Count the remaining lock reentrant times
if(Thread.currentThread() ! = getExclusiveOwnerThread())// Check whether the current thread is the thread that acquired the lock, if not directly throw an exception
throw new IllegalMonitorStateException();
boolean free = false;// Whether to release the lock, true- release false- do not release
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);// Clears the thread currently holding the lock
}
setState(c);// Change the lock count
return free;// return the lock release status
}
Copy the code
Check whether the current thread is an exclusive lock holder or not. If the value is 0, the current thread can release the lock and clear the identification of the exclusive lock thread. The value of state is set to 0 or the reentrant times of the remaining lock.
After the setState(0) method is executed, the lock has been released. At this time, a new thread can get the lock, because the unfair lock process has been analyzed before, and the state value should be modified through CAS before going online, and the lock will be obtained if the modification is successful.
In the most extreme case, an unfair lock can cause a thread that is already in the queue to never acquire the lock, which is the lock starvation problem. However, the probability of this happening is relatively small, can be ignored. If there is a large amount of concurrency in actual production, this problem needs to be solved. Fair lock can be used. When no thread obtains the lock, it needs to check whether there is a valid node in the queue first. This ensures that the lock is acquired first in the queue.
The main idea of a fair lock is the same as that of an unfair lock, but the method of trying to acquire the lock is different. This will not be analyzed in detail, and we will mainly look at the tryAcquire method:
public class Lock_01 {
public static void main(String[] args) {
Lock lock = new ReentrantLock(true);ReentrantLock(true) Fair lock, ReentrantLock(false) Unfair locklock.lock(); lock.unlock(); }}Copy the code
// java.util.concurrent.locks.ReentrantLock
public void lock(a) {
sync.lock();
}
Copy the code
// java.util.concurrent.locks.ReentrantLock#FairSync
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if(! hasQueuedPredecessors() &&// The main difference between the locking process and the unfair locking process is that it is necessary to check whether there are valid nodes in the queue first. Only when there are no valid nodes in the queue can the queue be added
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;
}
Copy the code
Analyse the HasqueuedToraise method:
public final boolean hasQueuedPredecessors(a) {
Node t = tail; // t points to the tail node
Node h = head; // h points to the head node
Node s;
returnh ! = t &&// h! If =t, there are nodes in the queue
((s = h.next) == null|| s.thread ! = Thread.currentThread()); }Copy the code
The core of this method is the last line of code, H! If =t, there are nodes in the queue. H.ext ==null. When does this happen? To understand this, consider the steps to add nodes to the queue:
- Node. prev = pred, where the precursor of the current node points to tail
- CompareAndSetTail (pred, node), the current node is set to tail
- Next = node, the next of the precursor node points to node
When 1,2 is executed successfully but 3 is not executed, the value of h.ext is null. If the successor node of the head node is not the current thread, it is enqueued. If the successor node is the current thread, it tries to obtain the lock by changing the state value through CAS.
3 AQS application
3.1 Application scenarios of AQS in JUC
Many concurrent tools in JUC are based on AQS. The following describes the application scenarios of several synchronization tools AQS:
Synchronization tool | Association between synchronization tools and AQS |
---|---|
ReentrantLock | Use AQS to save the number of locks. ReentrantLock After the lock is successfully added, the ID of the lock thread is obtained, which is used to determine the reentrant of the lock and prevent other threads from unlocking. |
CountDownLatch | Use AQS to synchronize status count. Every time countDown is executed, the AQS synchronization status value decreases by 1, and when it reaches 0, all blocked threads are woken up and can continue execution. |
Semaphore | Saves the current amount of semaphore using the AQS synchronization state. Each time acquire is executed, the current semaphore quantity decreases by 1. When it decreases to 0, the thread enters the AQS blocking queue and waits for other threads to execute release before waking up the thread on the head of the blocking queue. |
ReentrantReadWriteLock | AQS synchronization status state Is an int and occupies 4 bytes of 32 bits. It is the number of low 16-bit write locks and the number of high 16-bit read locks. |
3.2 Customizing synchronization Tools
Implement a custom synchronization tool is very simple, only needs to inherit AbstractQueuedSynchronizor, rewrite tryAcquire and tryRelease method, these two methods is the core of the locking and unlocking method, In addition, two Api interfaces lock and unlock are provided for adding and unlocking. Inside these two methods, acquire and release methods of AQS are directly called through custom synchronization tool objects.
// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
if(! tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
Copy the code
Through the source code can be seen that AQS internal tryAcquire method is not implemented, this method needs to custom synchronizer itself to implement. In AQS, acquire method calls tryAcquire of subclass, so tryAcquire is a hook method, which needs to inherit the subclass implementation of AQS, and acquire method adopts template method design mode.
// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if(h ! =null&& h.waitStatus ! =0)
unparkSuccessor(h);
return true;
}
return false;
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
Copy the code
The tryRelease method works the same as the tryAcquire method above.
According to the above analysis, to implement a simple version of the custom synchronizer MyLock:
public class MyLock {
private final Sync sync = new Sync();
public void lock(a) {
sync.acquire(1);
}
public void unlock(a) {
sync.release(0);
}
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
return compareAndSetState(0, arg);
}
@Override
protected boolean tryRelease(int arg) {
setState(arg);
return true; }}}Copy the code
MyLock:
public class MyLockTest {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
MyLock lock = new MyLock();
Runnable r = () -> {
try {
lock.lock();
for (int i = 0; i < 1000000; i++) { count++; }}finally{ lock.unlock(); }}; Thread t1 =new Thread(r);
Thread t2 = newThread(r); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); }}Copy the code
No matter how many times you run it, the final output is: 2,000,000
References:
- Tech.meituan.com/2019/12/05/…
- www.cnblogs.com/takumicx/p/…