Learn AQS with ReentrantLock
java and contract( java.util.concurrent) is a powerful and extensible tool class for concurrency management provided by jdk. The main source code author is Doug lea (Doug Lee), a well-known concurrency programming master. The strong expansibility of the whole juc and the essence of concurrent programming thought are In the basic class AbstractQueuedSynchronizer, we can know from the name of this class that it is a queue synchronizer. Yes, the synchronization management of threads in juc is through a queue.
This is a first in first out queue. The waiting process of a thread is to queue in this queue. After the execution of a node (thread) in the queue, it is the turn of the next node. After the execution, the thread will delete its own node from the queue.
1. AQS Foundation
1.1 state variable – the embodiment of AQS's strong expansibility
A key attribute in AQS is the following state, which marks the synchronization status. Why is it the embodiment of AQS's strong expansibility? Because in different concurrent tools implemented based on AQS, state means different meanings, and the functions of these concurrent tools are carried out around state, such as:
- In ReentrantLock, state indicates the number of times the current lock has been re entered. Only when state is equal to 0 can another thread obtain the lock.
- In ReentrantReadWriteLock, the high and low 16 bits of state indicate the number of read locks acquired and the number of write locks re entered.
- In Semaphore, it indicates the number of licenses that can be issued.
Therefore, because juc uses state flexibly and based on the basic thread queue management function provided by AQS, it makes juc powerful. At the same time, note that this state is modified by volatile to ensure the visibility between threads.
private volatile int state;
1.2 data structure definition of AQS queue node
+------+ prev +-----+ +-----+ | | <---- | | <---- | | tail +------+ +-----+ +-----+
Doug Li wrote in detail about the data structure of AQS nodes in the notes to the source code. The specific structure is a two-way linked list with Node as the Node. The above is the display of the data structure of the linked list in the source code. Let's learn about the data structure of Node.
The first is the state of the lock. We know that the lock is divided into read lock and write lock. The write lock is exclusive, that is, when a thread obtains the write lock of the object, other threads will fail to try or write lock or read lock. When a thread obtains the read lock of the object, Then other threads can obtain the read lock of this object, but cannot obtain the write lock.
Read lock | Write lock | |
---|---|---|
Read lock | √ | × |
Write lock | × | × |
In AQS, SHARED indicates SHARED status, EXCLUSIVE indicates EXCLUSIVE status, and corresponds to read lock and write lock scenarios respectively.
In addition to the lock state, the Node also defines a very important attribute, which is the waiting state of the Node. This can also be understood as the waiting state of the thread, but this state is java level, not the thread state of the operating system level.
-
CANCELLED (1): marks that the current thread is CANCELLED, that is, the node in the current queue is abandoned, and the thread in it does not need to be executed.
-
SIGNAL (- 1): the next node marking the current node is blocked by park. The current thread needs to use unpark to wake up the thread of the next node when releasing the lock.
-
Condition (- 2): mark that the current node is waiting in the condition queue (the node waiting with condition).
-
PROPAGATE (- 3): indicates that the current node needs to wake up all waiting nodes (this is the thread waiting to obtain the read lock after the head node wakes up in the read-write lock).
It can be seen that all these states are int tag values. At the same time, the nodes in CANCELLED state are not available. Therefore, the way to judge whether a node is available in AQS is to judge whether the waitState of the node is greater than 0.
Of course, the Node node also encapsulates the thread that the current Node is waiting for, as well as the pointers of the previous and subsequent nodes that a linked list Node should have.
static final class Node { //-------Lock status------// /** Mark this node as shared mode */ static final Node SHARED = new Node(); /** Mark this node as exclusive */ static final Node EXCLUSIVE = null; //---------Wait state of current node----------// /** Wait state: thread canceled */ static final int CANCELLED = 1; /** Waiting status: the successor thread marking the current node needs to be awakened by the current thread */ static final int SIGNAL = -1; /** Wait status: marks that the current thread is waiting in a condition */ static final int CONDITION = -2; /** Wait state: the marked node needs to be propagated unconditionally. It can only exist in the state of the head node. The scene is read The write lock in the write lock wakes up all waiting read threads */ static final int PROPAGATE = -3; /** Waiting state of node */ volatile int waitStatus; /** Pointer to the successor node of the current node */ volatile Node prev; /** Pointer to the successor node of the current node */ volatile Node next; /** Threads in the current node */ volatile Thread thread; /** Next waiting thread */ Node nextWaiter; }
1.3 support of native method for AQS
The threads running in the program we write are all user state threads. To make the user state thread enter the waiting state, you need to make the user state thread enter the kernel state. At the same time, the wake-up operation needs to switch the threads up and down. These operations are supported by the operating system. java code runs on the virtual machine and cannot directly call the operating system, This part is implemented through the native method provided by jdk, that is, Unsafe class.
Unsafe provides many native methods. For understanding AQS, we only need to know some of them.
The first is the park method, which literally means parking. This is the native method that allows the thread to enter the waiting state. AQS calls this method to operate the thread in a practical sense. Of course, it calls the encapsulation method of park by LockSupport class.
public native void park(boolean var1, long var2);
With park, of course, unpark is indispensable. This is the method to wake up the thread.
public native void unpark(Object var1);
There is also the compareAndSwapInt method, which provides atomic cas at the operating system instruction level. Note that this method is atomic. This is also a native method. What is passed in is the address of the object, the variable to be changed, the memory offset, expected value and target value of the initial address of the object. cas doesn't explain much here. It mainly remembers an ABA problem and the version number mechanism.
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
The whole Unsafe class provides native methods for atomic operations on objects. Basically, the operation methods are to locate attributes according to the offset of attributes in object memory, and then operate on some columns. This style is very similar to C language.
2. Reentrant lock acquisition and release
ReentrantLock is implemented based on AQS. Some extensions are made through the general thread management framework provided by AQS. The specific extensions are reflected in the fact that ReentrantLock class inherits AQS and implements a synchronizer Sync, but the Sync itself is abstract, Another two classes inherit this synchronizer and implement two synchronizers that obtain locks fairly and unfairly, namely FairSync and NonFairSync. ReentrantLock uses the unfair lock acquisition method by default. I will explain the lock acquisition of these two methods here.
2.1 queue synchronizer
As mentioned above, the queue synchronizer that can be used by ReentrantLock and AQS actually have a two-tier inheritance relationship. The code calling process in this part is very winding. It will be very confused when you look at it for the first time. One is the internal subclass calling the parent class, and the other is the parent class calling the internal subclass. I think this style will only appear in this source code, Usually the code we write is concise and clear.
The queue synchronizer inherits from AQS and mainly implements the tryAcquire() and tryRelease() methods. For example, in a reentrant lock, acquiring a lock through the lock method actually calls the acquire method of the synchronizer's parent class, but the specific logic of acquiring a lock is in the tryAcquire() implemented by its subclass. Releasing the lock is the same. It calls the release() method of the parent synchronizer, and specifically calls the tryRelease() method implemented by its subclass.
It can be understood that AQS is a synchronizer, which maintains a FIFO queue. The synchronization of threads is actually completed through the thread node in the synchronizer operation queue.
2.2 obtaining locks
Lock acquisition flowchart:
Here, I will introduce the main method logic following the steps of the above flowchart.
The first is to obtain the lock directly. The way to obtain the lock is to set the state of AQS through cas. We have talked about the meaning of state in ReentrantLock. The expected value here is 0 and the target value is 1, that is, what the current thread thinks is that the current lock has not been obtained. If the cas operation is successful, set the exclusive thread of the current lock as the current thread. If it can run here, it means that the current lock has not been obtained by other threads, so the thread can obtain the lock naturally. If the lock is obtained here, the whole lock acquisition process will end.
However, in a concurrent scenario, it may not be so easy to obtain a lock, so you need to execute the following logic, that is, the logic to obtain a lock.
final void lock() { if (compareAndSetState(0, 1)) //If cas successfully obtains the lock, set the exclusive thread as the current thread setExclusiveOwnerThread(Thread.currentThread()); else //cas continues to execute the logic of acquiring locks when it fails to acquire locks acquire(1); }
The acquire method is the method to acquire the lock after the current lock has been acquired by other threads.
public final void acquire(int arg) { //Try to acquire the lock directly and spin to acquire the lock after joining the team if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //Interrupt your own thread selfInterrupt(); }
Let's take a look at the tryAcquire method first. From the method name, we can know that this method is trying to obtain a lock. The condition for this method to obtain a lock is that the current lock and the lock are released by the last thread. If it is not held, or the thread currently holding the lock is the current thread, then re-enter.
The logic here is also a concrete embodiment of reentrant lock. When the thread currently holding the lock is the thread currently trying to obtain the lock, it directly sets state plus one. In ReentrantLock, the value of state indicates the number of times the current lock has been re entered. If it is 0, it means that the current lock is not held by any thread. Each time the thread re enters the lock, the state will increase by one. Similarly, each time the lock is released, the state will decrease by one. That is to say, each time the lock method is called, a corresponding unLock method is required, otherwise the lock cannot be released.
final boolean nonfairTryAcquire(int acquires) { //Gets the current thread final Thread current = Thread.currentThread(); //Get current lock status int c = getState(); if (c == 0) { //If the lock is released, try to modify the state of the lock through cas if (compareAndSetState(0, acquires)) { //Successful modification means successful lock acquisition. The exclusive thread setting the current lock is the current thread setExclusiveOwnerThread(current); return true; } } //If the current thread is the thread holding the lock, it is now a reentry else if (current == getExclusiveOwnerThread()) { //Set the lock state plus one, that is, reentry once int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); //Sets the current state of the lock setState(nextc); return true; } //The attempt to acquire the lock failed return false; }
When the lock is not obtained in the above steps, the thread needs to be encapsulated as a Node and placed in the AQS queue for waiting. The nodes put into the queue are placed at the end of the queue, which is also in line with our common sense of queuing.
private Node addWaiter(Node mode) { //Call the construction method of Node to encapsulate the current thread into a Node Node node = new Node(Thread.currentThread(), mode); // Gets the tail node of the current queue Node pred = tail; if (pred != null) { node.prev = pred; //cas sets the tail node of the queue as the current node if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //If the queue is empty, the queue is initialized enq(node); //Returns the current node return node; }
The queue initialization method calls the enq method, which will not be expanded here. The logic is very simple, that is, first create an empty node as the head node, and then put the node of the current thread after the empty node as the tail node.
After completing the above steps, the node of the current thread has been placed in the queue, and then the acquirequeueueueued method will be executed to let the thread try to obtain the lock in the queue. This step is more abstract. I'll borrow the illustration of this step in the art of concurrent programming:
The above figure can be described vividly. All threads encapsulated as nodes spin in the queue. The purpose of spin is to try to obtain locks. In fact, this figure may have other states. Maybe all node spin states after the head node stop, Because after the attempt to acquire the lock fails, the thread will block itself and wait for the wake-up after the previous node releases the lock.
Let's take a detailed look at the code implementation of the above steps. What is called here is the acquirequeueueueued method, which literally means to obtain the lock in the queue. Yes, the threads that obtain the lock in this step are in the queue. The general state of the whole AQS queue is shown in the figure above. You can see that there is an dead loop in the method, which is the spin operation. So what is the logic of spin lock acquisition or self blocking?
First, the current node obtains the previous node P, and then determines whether this p is the head node. If it is the head node, then you can try to obtain the lock. If the lock is obtained successfully, the head node of the queue is set as the current node, and the previous head node is cleared from the queue.
If the previous node is not a head node or is a head node but the lock acquisition fails, the current thread needs to be blocked. Judge whether the current thread should be blocked by calling shouldParkAfterFailedAcquire method. If possible, it will be blocked by LockSupport's park. Then, if the current node is awakened by the head node that releases the lock, why does it fail to preempt the lock? This is because of the characteristics of preemptive lock acquisition. After the current thread is awakened by the thread of the header node, another thread not in the queue may try to acquire the lock, and the acquisition is just successful. Then the current thread can only continue to wait for the thread that preempts the lock to wake up after releasing the lock, so as to continue to preempt the lock, If the concurrency is particularly high, this situation will occur frequently, resulting in the thread in the queue never being executed. This phenomenon is thread hunger, so preemptive lock acquisition may lead to thread hunger.
final boolean acquireQueued(final Node node, int arg) { //Flag indicating whether the lock acquisition failed boolean failed = true; try { //Thread interrupt flag boolean interrupted = false; //Always cycle through the process of obtaining locks for (;;) { //Get the predecessor node of the current node first. This method may throw an exception final Node p = node.predecessor(); //If the predecessor node is the head node, it means that the queue has been queued to the current node, then try to obtain the lock at this time if (p == head && tryAcquire(arg)) { //Set the current node as the head node of the queue. In this step, the thread in the head node is directly cleared setHead(node); //This is to help GC recycle because the previous header node reference is unreachable p.next = null; // help GC failed = false; //If the lock is obtained successfully, it will be returned directly return interrupted; } //Here, it is determined whether the current node should be park when it fails to obtain the lock, //If so, the current thread is directly put into the waiting state, and //Check whether the current thread is interrupted when it is awakened //park if you need it if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { //Judge whether the lock acquisition fails. If the acquisition fails, the reason is that the code throws an exception during the acquisition process if (failed) //Cancel the lock acquisition, set the information of the current node and adjust the queue cancelAcquire(node); } }
As mentioned above, when a thread fails to obtain a lock in the queue, it needs to call park to make it enter the waiting state. Before self waiting, the current thread needs to judge whether it should enter the waiting state and what is the meaning of whether it should enter the waiting state? We all know that after the thread enters the waiting state, it needs to be awakened by other threads, otherwise it will always be in the waiting state. Then the task of waking up the current thread is handed over to the task of the predecessor node of the current node, but we don't know the state of the predecessor node. Maybe the thread of the predecessor node is terminated? Therefore, it is necessary to judge the status of the predecessor node. Therefore, shouldParkAfterFailedAcquire is to ensure that a thread will wake up when the current thread is waiting.
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //Gets the waiting state of the immediate predecessor node of the current node int ws = pred.waitStatus; //If the status of the current node is SIGNAL, there is nothing to worry about if (ws == Node.SIGNAL) //Returning true indicates that you can safely enter the waiting state return true; if (ws > 0) { //If the status of the direct predecessor node is cancelled, it means that the predecessor node cannot wake up the current thread //Here, a loop is used to look forward from the direct successor node until the waiting state of a node is found //Cancels the node and takes the current node as the direct successor of the found node do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //This indicates that the status of the direct predecessor node is not cancelled but not SIGNAL, but in the predecessor node //The thread can run. Here, the waiting state of the predecessor node is set to SIGNAL directly through cas //This is equivalent to telling the previous node that it needs to wake up the current thread after the execution is completed compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
The way to get the thread into the blocking state is parkAndCheckInterrupt, which can be used to determine whether the thread can be safely waiting. After calling locksupport park(this); After this code, the process executed by the current thread stops directly on this line of code. After being awakened, the program will start to execute on the lower line of this line of code through thread interrupted(); To check whether the thread is set to interrupt while waiting.
private final boolean parkAndCheckInterrupt() { //Make the current thread wait LockSupport.park(this); //Check whether the thread is interrupted during waiting return Thread.interrupted(); }
Let's go back to the acquirequeueueueueueueueued method. We can see that the spin operation is wrapped by a try finally. The first way for this optional operation to leave the spin is to obtain the lock and return successfully, and the other is to throw an exception during the spin process. After obtaining the lock successfully, the previous head node will disconnect the queue, and then the current node will become the head node. Threads are not saved in the head node.
Then, after throwing an exception, the program will not return immediately, but needs to clean up the queue after giving up obtaining the lock. One of the tasks of cleaning up is to remove its own node from the queue, and the other is to ensure that the successor nodes of the current node can be safely awakened.
private void cancelAcquire(Node node) { if (node == null) return; node.thread = null; // Find the successor node of the current node Node pred = node.prev; //Find the first node whose waiting status is not cancelled from the back to the front, and replace its predecessor with the current node, //The successor node that is the successor of the predecessor until the predecessor node of the current node is not in the cancelled state while (pred.waitStatus > 0) node.prev = pred = pred.prev; //Obtain the successor node of the found predecessor node. Note that the successor node is still the original node at this time. We just //The predecessor node pointer of the current node points to the found node, that is, our predecessor node is found here //But the successor node of the found node is not us Node predNext = pred.next; //Set the waiting status of the current node to cancel so that other nodes can skip the current node. In our complete //Before releasing the current thread node.waitStatus = Node.CANCELLED; if (node == tail && compareAndSetTail(node, pred)) { //If we are the tail node, cas can set the found predecessor node as the tail node, //And set its next to null compareAndSetNext(pred, predNext, null); } else { int ws; if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { //If the predecessor node is not in the cancel state and is in the SIGNAL state and the thread is not empty, //Then cas sets the successor node of the predecessor node as the successor node of the current node Node next = node.next; if (next != null && next.waitStatus <= 0) compareAndSetNext(pred, predNext, next); } else { //If the above conditions are not met, wake up the successor node and let it start to acquire the spin lock resources unparkSuccessor(node); } //Make your reference unreachable and help GC node.next = node; } }
As mentioned above, when the current node is removed from the preparation queue, if the predecessor node of the current node cannot wake up its successor node effectively and safely, the current thread needs to be responsible for waking up its successor node. This part of the logic is placed in the unparksuccess method. The specific process is to traverse forward from the tail node until you find a wake-up node closest to the current node and wake it up.
private void unparkSuccessor(Node node) { //Set the wait state of the current node to 0 int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); //Start from the end of the queue and traverse forward until you find a node whose status is not cancelled closest to the current node 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) //Wake up found nodes LockSupport.unpark(s.thread); }
2.3 release lock
The release of locks is neither fair nor unfair, and the logic of releasing locks is also very simple.
The unlock() method calls the release method of the abstract synchronizer, and the release method executes the specific lock release logic in the tryRelease method. tryRelease, like the tryAcquire method, is an empty implementation in AQS and a template method provided to subclasses for implementation. Different concurrent tools in juc have their own implementation in tryRelease.
public final boolean release(int arg) { //Call the subclass's release lock method if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) //Wake up the successor node unparkSuccessor(h); return true; } return false; }
Let's take a look at the implementation of tryRelease in ReentrantLock. The specific logic is to judge whether the thread currently releasing the lock is the thread occupying the lock. If so, calculate the reentry times after the lock is released once, and take the reentry times as the value of state. If the state is equal to 0 after the lock is released, the lock is completely released.
protected final boolean tryRelease(int releases) { //releases is the number of re-entry times released. In the re-entry lock, it is 1. Here is the calculation //The number of lock reentries after the lock is released once, that is, the value of AQS state after the lock is released int c = getState() - releases; //If the thread releasing the lock is not the thread currently occupying the lock, an error is reported if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; //If the number of reentries after releasing the lock is 0, it means that the lock has been completely released if (c == 0) { free = true; //Set the exclusive thread of the current lock to null setExclusiveOwnerThread(null); } //Set the state of the current AQS. The reason why CAS is not used here is because it is executed here //The thread can only be the thread that acquires the lock setState(c); return free; }
2.4 difference between fair and unfair
We talked about the difference between fair and unfair reentrant locks. Let's take a look at the difference between the execution processes of the two codes.
- First, the specific processes of fair and unfair execution are in two different synchronizers. They both inherit AQS and rewrite the tryAcquire method.
First, obtain the lock fairly. You can see that when obtaining the lock, you do not directly modify the current lock state through cas, but first judge whether there are threads in the current queue. If so, you do not obtain the lock. This is the core of fair access to locks.
Instead of obtaining locks fairly, as we said above, in tryAcquire, no matter whether there are threads in the queue or not, it directly modifies the state value through cas. If the modification is successful, it will obtain the lock.
ReentrantLock is a non fair lock by default.
protected final boolean tryAcquire(int acquires) { //Get current thread final Thread current = Thread.currentThread(); //Gets the current number of lock reentries int c = getState(); //If the lock is not acquired if (c == 0) { //Judge whether there are waiting threads in the current queue, and cas sets state successfully, //If the exclusive thread is set successfully, the lock is obtained successfully if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //If the current lock is acquired and the thread that acquires the lock is the current thread, then re-enter else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
2.5 using AQS to implement a synchronization tool
Through the above learning, we can implement a thread synchronization tool through AQS. In some business scenarios, we may need to have only one thread executing a process, so we can simply implement a non reentrant lock function according to this.
Let's talk about the overall class structure first. Because the thread synchronization tool through AQS is relatively flexible, this uses the class diagram to describe the relationship between classes.
The first is our NonReentrantLock class, which is the actual tool class. As a Lock, it needs to implement the Lock interface, including lock(), unlock(), lockinterruptible(), tryLock(), tryLock(long time, TimeUnit unit), and newCondition(). But we only implement the Lock () and unlock () methods.
Class diagram:
Sequence diagram for acquiring and releasing locks:
Implementation of synchronizer:
package com.ducx.playground.client; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.AbstractQueuedSynchronizer; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; /** * Non reentrant lock via AQS */ public class NonReentrantLock implements Lock { //Instantiate one of our custom synchronizers private NonfairSync nonfairSync = new NonfairSync(); //------------------------Implementation method of lock interface---------------------------// /** * Method of obtaining lock */ @Override public void lock() { nonfairSync.lock(); } /** * Method of releasing lock */ @Override public void unlock() { nonfairSync.unLock(); } @Override public void lockInterruptibly() throws InterruptedException { } @Override public boolean tryLock() { return false; } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return false; } @Override public Condition newCondition() { return null; } //------------------------Implementation method of lock interface---------------------------// /** * Unfair non reentrant queue synchronizer */ static final class NonfairSync extends AbstractQueuedSynchronizer { /** * Method for obtaining lock by synchronizer */ public void lock(){ acquire(1); } /** * Method of releasing synchronizer lock */ public void unLock(){ release(1); } /** * Method of trying to acquire lock * @param arg * @return */ @Override protected boolean tryAcquire(int arg) { //Compare the thread occupied by the current lock with the current thread. If it is the same, it means that the current thread does not support it //Then throw an exception if(getExclusiveOwnerThread() == Thread.currentThread()){ throw new UnsupportedOperationException("This lock can not reentrant!"); } //Modify the state of cas directly to obtain locks unfairly if(compareAndSetState(0, 1)){ //Lock acquisition succeeded. The exclusive thread setting the current lock is the current thread setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } /** * Method of trying to release the lock * @param arg * @return */ @Override protected boolean tryRelease(int arg) { //Determines whether the current thread is the exclusive thread of the lock if(getExclusiveOwnerThread() == Thread.currentThread()){ //Set state to 0 to release setState(0); //Set the exclusive thread of the current lock to null setExclusiveOwnerThread(null); return true; }else{ //If it is not the thread currently holding the lock to release the lock, an exception is thrown throw new UnsupportedOperationException("This thread is not the lock owner!"); } } } }
Test method:
package com.ducx.playground.client; public class MutiThreadClient { public static void main(String[] args) throws Exception{ //Instantiate a custom non reentrant lock NonReentrantLock nonReentrantLock = new NonReentrantLock(); try{ //Acquire lock nonReentrantLock.lock(); System.out.println("Thread acquired lock successfully!"); //The current thread acquires the lock again nonReentrantLock.lock(); System.out.println("Thread acquired lock successfully again!"); }catch (Exception e){ e.printStackTrace(); }finally { //Release the lock twice nonReentrantLock.unlock(); nonReentrantLock.unlock(); } } }
Test results: we can see that we tried to use this non reentrant lock as a reentrant lock. The first time we successfully obtained the lock, and an exception will be thrown if the current thread is not the thread holding the current lock.