Original website: Java multithreading -- ReentrantLock -- use / example / principle_ CSDN blog
brief introduction
explain
This article introduces ReentrantLock (reentrant exclusive lock) in Java's JUC. Including: usage and principle.
summary
ReentrantLock is mainly implemented by CAS+AQS queue. It supports fair locks and unfair locks, and their implementation is similar.
Because CAS is used, it has the advantages and disadvantages of CAS: high performance and high CPU consumption.
ReentrantLock (reentrant exclusive lock): when state is initialized to 0, it indicates that it is not locked. When thread A locks (), it will call tryAcquire() exclusive lock and set state+1. Then other threads will fail when they think about tryAcquire again. Other threads will not have the opportunity to obtain the lock until thread A unlock s() to state=0. Before A releases the lock, it can also acquire the lock repeatedly (state accumulation), which is the concept of reentry. Note: you must release the lock as many times as you obtain the lock to ensure that the state can return to the zero state.
Example
private Lock lock = new ReentrantLock(); public void test(){ lock.lock(); try{ doSomeThing(); }catch (Exception e){ // ignored }finally { lock.unlock(); } }
Fairness and unfairness
The default implementation of ReentrantLock is a non fair lock, but it can also be set to a fair lock.
- Unfair lock
- If another thread comes in to try to get it at the same time, it may let this thread get it first;
- Fair lock
- If another thread comes in at the same time to try to obtain the lock, when it finds that it is not at the head of the queue, it will queue to the end of the queue and the thread at the head of the queue will obtain the lock.
ReentrantLock provides two constructors:
public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
lock() method of NonfairSync
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
First, use a CAS operation to determine whether the state is 0 (indicating that the current lock is not occupied). If it is 0, set it to 1, and set the current thread as the exclusive thread of the lock, indicating that the lock is obtained successfully. When multiple threads try to occupy the same lock at the same time, CAS operation can only ensure the success of one thread, and the rest can only be queued.
"Unfairness" is reflected here. If the thread occupying the lock just releases the lock and the state is set to 0, and the thread queuing for the lock has not woken up, the new thread directly preempts the lock, then it will "jump in the queue".
lock() method of FairSync
final void lock() { acquire(1); }
Call the acquire(1) method directly.
Principle of unfair lock
scene
Brief description: thread A obtains the lock, threads B and C fail, and threads B and C execute acquire(1);
This is explained by the example of non fair lock (NonfairSync). It is assumed that there are the following scenarios: three threads compete for the lock. Assuming that the CAS operation of thread A succeeds and returns happily after obtaining the lock, threads B and C fail to set state and go to else.
lock() method of NonfairSync
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
acquire() method
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
(1) Attempt to acquire lock
Description: try to acquire the lock. If the attempt to acquire the lock is successful, the method returns directly.
The process of non fair lock tryAcquire is as follows:
- Check the state field;
- If it is 0, it means that the lock is not occupied, then try to occupy it;
If it is not 0, check whether the current lock is occupied by itself. If so, state+1 (number of times to re-enter the lock). - If the above two points fail, obtaining the lock fails and returns false.
tryAcquire(arg) final boolean nonfairTryAcquire(int acquires) { //Get current thread final Thread current = Thread.currentThread(); //Get state variable value int c = getState(); if (c == 0) { //No threads occupy locks if (compareAndSetState(0, acquires)) { //Lock occupation succeeded. Set the exclusive thread as the current thread setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { //The lock is already occupied by the current thread int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); // Update the state value to the new number of reentries setState(nextc); return true; } //Failed to acquire lock return false; }
(2) Join the team
As mentioned above, thread A has occupied the lock, so B and C fail to execute tryAcquire and enter the waiting queue (acquired queue (addwaiter (node. Exclusive), Arg)). If thread A holds the lock, then B and C will be suspended.
Let's take a look at the process of joining the team. First look at addWaiter(Node.EXCLUSIVE)
/** * Associate the new node with the current thread and queue it * @param mode Exclusive / shared * @return New node */ private Node addWaiter(Node mode) { //Initialize the node and set the associated thread and mode (exclusive or shared) Node node = new Node(Thread.currentThread(), mode); // Get tail node reference Node pred = tail; // The tail node is not empty, indicating that the queue has been initialized if (pred != null) { node.prev = pred; // Set the new node as the tail node if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // If the tail node is empty, it means that the queue has not been initialized. You need to initialize the head node and join the new node enq(node); return node; }
B. C thread tries to enter the queue at the same time. Since the queue has not been initialized and tail==null, at least one thread will go to enq(node). Let's assume that we go to enq(node) at the same time.
/** * Initialize the queue and queue up new nodes */ private Node enq(final Node node) { //Start spin for (;;) { Node t = tail; if (t == null) { // Must initialize // If the tail is empty, a new head node will be created, and the tail points to the head if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; // tail is not empty. Join the new node in the queue if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
The classical spin + CAS combination is embodied here to realize non blocking atomic operation. Since the implementation of compareAndSetHead uses the CAS operation provided by the unsafe class, only one thread will create the head node successfully. Suppose that thread B succeeds, and then B and C start the second cycle. At this time, the tail is not empty, and both threads go to else. Assuming that the compareAndSetTail of the B thread succeeds, then B can return, and C needs a third round of loop due to the queue failure. Finally, all threads can be successfully queued.
When B and C enter the waiting queue, the AQS queue is as follows:
(3) Hang
B and C execute acquirequeueueueued (final node, int ARG) successively. This method allows queued threads to attempt to acquire locks, and if they fail, they will be suspended.
/** * A queued thread attempted to acquire a lock */ final boolean acquireQueued(final Node node, int arg) { boolean failed = true; //Marks whether the lock was successfully acquired try { boolean interrupted = false; //Mark whether the thread has been interrupted for (;;) { final Node p = node.predecessor(); //Get precursor node //If the precursor is head, that is, the node has become the second, then it is qualified to try to obtain the lock if (p == head && tryAcquire(arg)) { setHead(node); // Successfully obtained. Set the current node as the head node p.next = null; // The original head node goes out of the queue and is recycled by GC at a certain time point failed = false; //Get success return interrupted; //Whether the return has been interrupted } // Judge whether the acquisition can be suspended after failure. If yes, suspend if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // If the thread is interrupted, set interrupted to true interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
The comments in code have clearly explained the implementation process of acquirequeueueueed. A ssuming that B and C hold the lock all the time while competing for the lock, their tryAcquire operation will fail, so they will go to the second if statement.
Take another look at the shouldParkAfterFailedAcquire and parkAndCheckInterrupt processes
/** * Judge whether the current thread needs to suspend after it fails to acquire the lock */ private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //Status of the precursor node int ws = pred.waitStatus; if (ws == Node.SIGNAL) // The status of the precursor node is signal and returns true return true; // The status of the precursor node is CANCELLED if (ws > 0) { // Look for the first node whose status is not CANCELLED from the end of the queue do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { // Set the status of the precursor node to SIGNAL compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } /** * Suspend the current thread, return the thread interrupt status and reset */ private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
The premise that a thread can hang after joining the queue is that the state of its precursor node is SIGNAL, which means: "Hi, brother in front, if you get the lock and leave the queue, remember to wake me up!". Therefore, shouldParkAfterFailedAcquire will first determine whether the current node's predecessor meets the requirements. If it meets, it will return to true, then call parkAndCheckInterrupt to hang itself. If not, check whether the precursor node is > 0 (cancelled). If so, traverse forward until the first precursor that meets the requirements (the state is not greater than 0) is found. If not, set the state of the precursor node to SIGNAL.
In the whole process, if the status of the precursor node is not SIGNAL, you can't hang at ease. You need to find a safe starting point. At the same time, you can try again to see if there is a chance to try the competitive lock.
The final queue may look like the following figure
summary
Use a flowchart to summarize the lock acquisition process of unfair locks.
Principle of unfair lock
public void unlock() { sync.release(1); } public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
If you understand the process of locking, unlocking seems much easier. The process is basically to try to release the lock first. If the release is successful, check whether the status of the head node is SIGNAL. If so, wake up the thread associated with the next node of the head node. If the release fails, return false to indicate that the unlocking fails. Here we also found that each time only the thread associated with the next node of the head node is called.
Finally, let's take a look at the implementation process of tryRelease
/** * Release the lock occupied by the current thread * @param releases * @return Release successful */ protected final boolean tryRelease(int releases) { // Calculate the state value after release int c = getState() - releases; // If the lock is not occupied by the current thread, an exception is thrown if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { // The number of lock re-entry times is 0, indicating that the release is successful free = true; // Empty exclusive thread setExclusiveOwnerThread(null); } // Update state value setState(c); return free; }
The input parameter here is 1. The process of tryRelease is: if the thread currently releasing the lock does not hold the lock, an exception will be thrown. If the lock is held, calculate whether the released state value is 0. If it is 0, it indicates that the lock has been successfully released, and the exclusive thread is cleared. Finally, update the state value and return free.
Fair lock principle
The difference between a fair lock and a non fair lock is that when a fair lock obtains a lock, it does not check the state first, but directly executes aqcuire(1);
Timeout mechanism
The tryLock(long timeout, TimeUnit unit) of ReetrantLock provides the function of obtaining locks over time. Its semantics is to return true if a lock is obtained within a specified time, and false if it is not obtained. This mechanism prevents threads from waiting indefinitely for lock release. So how is the timeout function implemented? Let's take unfair lock as an example to find out.
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); }
Or called the methods in the inner class. Let's move on
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); }
The semantics here is: if the thread is interrupted, throw InterruptedException directly. If it is not interrupted, try to acquire the lock first. If the acquisition is successful, it will return directly. If the acquisition fails, enter doAcquireNanos. We've seen tryAcquire. Here we'll focus on what doacquire nanos does.
/** * Compete for locks in a limited time * @return Is it successful */ private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { // Start time long lastTime = System.nanoTime(); // Thread queue final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { // Spin again! for (;;) { // Get precursor node final Node p = node.predecessor(); // If the precursor is the head node and the lock is occupied successfully, the current node will become the head node if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return true; } // If it has timed out, false is returned if (nanosTimeout <= 0) return false; // The timeout has not expired and needs to be suspended if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) // Blocks the current thread until the timeout expires LockSupport.parkNanos(this, nanosTimeout); long now = System.nanoTime(); // Update nanosTimeout nanosTimeout -= now - lastTime; lastTime = now; if (Thread.interrupted()) //Corresponding interrupt throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
The process of doAcquireNanos is briefly described as follows: the thread enters the waiting queue first, then starts to spin, tries to obtain the lock, and returns when it succeeds. If it fails, find a safe point in the queue and hang itself until the timeout expires. Why do we need a cycle here? Because the precursor state of the current thread node may not be SIGNAL, the thread will not be suspended in the current round of loop, and then update the timeout to start a new round of attempts
Polling and interrupt
The reason why ReentrantLock is retained is that ReentrantLock has two more functions than synchronized: pollable and interruptible.
1. Pollable
The examples in the original book look complex, but the meaning is very simple. A transfer operation is either completed within the specified time, or tell the caller that the operation is not completed within the specified time. This example requires the pollable feature of ReentrantLock, that is, repeatedly trying to obtain a lock within the specified time. If the lock is successful, the transfer operation can be completed. If the lock is not obtained within the specified time, the transfer fails. If you use synchronized, you can't do it.
public boolean transferMoney(Account fromAcct, Account toAcct, DollarAmount amount, long timeout, TimeUnit unit) throws InsufficientFundsException, InterruptedException { long fixedDelay = getFixedDelayComponentNanos(timeout, unit); long randMod = getRandomDelayModulusNanos(timeout, unit); long stopTime = System.nanoTime() + unit.toNanos(timeout); while (true) { if (fromAcct.lock.tryLock()) { try { if (toAcct.lock.tryLock()) { try { if (fromAcct.getBalance().compareTo(amount) < 0) throw new InsufficientFundsException(); else { fromAcct.debit(amount); toAcct.credit(amount); return true; } } finally { toAcct.lock.unlock(); } } } finally { fromAcct.lock.unlock(); } } if (System.nanoTime() < stopTime) return false; NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod); } }
2. Interruptible
In synchronized code, the code entering the critical area cannot be interrupted, which is very inflexible. If we use a thread pool to distribute tasks, if a code occupies a lock for a long time, it will certainly affect other tasks in the thread pool. Therefore, adding an interrupt mechanism improves the control of tasks.
public boolean sendOnSharedLine(String message) throws InterruptedException { lock.lockInterruptibly(); try { return cancellableSendOnSharedLine(message); } finally { lock.unlock(); } } private boolean cancellableSendOnSharedLine(String message) throws InterruptedException { ... }
Fairness: ReentrantLock uses unfair locks by default, and synchronized locks also use unfair locks.
If you don't require pollable and interruptible locks, use the synchronized built-in lock.
Other web sites
ReentrantLock principle_ Java_ Long road, long water - CSDN blog
Use ReentrantLock with caution