This chapter mainly analyzes the implementation details of reentrant lock ReentrantLock
First, show the class diagram involved in the implementation of ReentrantLock
1.AQS introduction
AQS is the AbstractQueuedSynchronizer class in the figure. This class is the basis of the concurrency tool class. AQS defines a set of synchronizer framework for multi-threaded access to shared resources. Many synchronization class implementations depend on it, such as ReentrantLock/Semaphore/CountDownLatch.
(1) The member volatile int state in AQS represents shared resources. For example, when ReentrantLock locks, it will change the state from 0 to 1 in CAS mode, which means that the current ReentrantLock object has been held by a thread. In addition, AbstractOwnableSynchronizer class will record the held thread reference of the current resource,
AQS provides corresponding methods to obtain and modify state and resource holding threads:
protected final int getState() { return state; } protected final void setState(int newState) { state = newState; } protected final boolean compareAndSetState(int expect, int update) { // unsafe yes jdk Atomic modification tool class provided(CAS) return unsafe.compareAndSwapInt(this, stateOffset, expect, update); } protected final void setExclusiveOwnerThread(Thread thread) { exclusiveOwnerThread = thread; } protected final Thread getExclusiveOwnerThread() { return exclusiveOwnerThread; }
(2) AQS completes the queuing of threads through a built-in FIFO two-way queue (internally, the head and tail elements of the queue are recorded through the nodes, and the Node type of the element is Node type)
/** * Head of the wait queue, lazily initialized. Except for * initialization, it is modified only via method setHead. Note: * If head exists, its waitStatus is guaranteed not to be * CANCELLED. */ private transient volatile Node head; /** * Tail of the wait queue, lazily initialized. Modified only via * method enq to add new wait node. */ private transient volatile Node tail;
(3) The thread in the Node node stores the thread reference currently queued. SHARED in the Node node indicates that the marked thread is blocked and added to the queue because of the failure to obtain SHARED resources; EXCLUSIVE in Node indicates that the thread is blocked and added to the queue because it failed to obtain EXCLUSIVE resources. waitStatus indicates the waiting status of the current thread:
① CANCELLED=1: indicates that the thread needs to cancel waiting from the waiting queue because of interruption or waiting timeout;
② SIGNAL=-1: the slave node representing the current node is blocked due to the failure of competing for shared resources. When releasing the lock, the thread represented by the subsequent node should be awakened.
③ CONDITION=-2: indicates that the node is in the waiting queue (this refers to waiting on the condition of a lock. The principle of condition will be described below). When the thread holding the lock calls the signal() method of condition, the node will transfer from the waiting queue of the condition to the synchronization queue of the lock to compete for the lock. (Note: the synchronization queue here is the FIFO queue maintained by AQS, and the waiting queue is the queue associated with each condition.)
④ PROPAGTE=-3: it means that the next sharing status acquisition will be passed to the subsequent nodes to obtain the sharing synchronization status.
2. Analyze the locking and unlocking process of reentrantlock (unfair mode)
(1) Lock
Suppose there are existing threads t1,t2,t3,
① First, t1 thread calls the locking method of ReentrantLock:
ReentrantLock: public void lock() { sync.lock(); } public ReentrantLock() { sync = new NonfairSync(); }
It can be seen that the lock() method of ReentrantLock essentially calls the lock() method of its static internal class NonfairSync, and NonfairSync is a subclass of AQS. Continue to analyze the lock() method of NonfairSync:
final void lock() { (1)use CAS Method modification state,If you succeed, you will get the lock. If you fail, the lock has been occupied if (compareAndSetState(0, 1)) (2)Get the lock and change the occupied thread to the current thread setExclusiveOwnerThread(Thread.currentThread()); (3)Failed to acquire lock else acquire(1); }
Because t1 is the first thread to acquire the lock, CAS method will succeed in acquiring the lock successfully. The current state is modified to 1 and the occupied thread reference is set to t1
AQS status is shown in the figure below:
② Suppose that t2 thread calls the locking method of ReentrantLock at this time:
At this time, the CAS method fails to set state because the t1 thread has occupied the lock. At this time, the code enters the acquire(1) method, which is the template method defined by AQS:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
In the acquire method, first call the tryAcquire method, which is implemented by the AQS subclass. This method is implemented in NofairSync:
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }
In NofairSync, the nonfairTryAcquire method is called, which is defined in Sync:
final boolean nonfairTryAcquire(int acquires) { 1.Get current thread final Thread current = Thread.currentThread(); 2.Gets the status of the current lock int c = getState(); 3.c Is 0, which means that no thread is currently occupying the lock if (c == 0) { 3-1. use CAS Method settings state,Represents that the lock is occupied by an existing thread if (compareAndSetState(0, acquires)) { 3-2.Set thread occupied application setExclusiveOwnerThread(current); 3-3. Lock successfully, return true return true; } } 4. Determine whether the thread occupying the lock is the current thread (reentrant lock) else if (current == getExclusiveOwnerThread()) { 4-1.calculation state New value for int nextc = c + acquires; 4-2.exception handling if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); 4-3.Set new value setState(nextc); 4-4.Lock successfully, return true return true; } 5.Locking failed, return false return false; }
Because thread t1 already holds the lock, thread t2 will judge as false at 3, and 4 is the lock verification of reentrant lock. If thread t1 locks again, the state value will be updated to 2, but t2= t1, so the final locking fails and returns false
After false is returned, addWaiter(Node.EXCLUSIVE) method will be called. This method is to build an exclusive mode Node containing t2 thread reference in the FIFO queue in AQS:
private Node addWaiter(Node mode) { 1.The current thread and the blocking reason(Because SHARED Pattern acquisition state Failure or EXCLUSIVE Acquisition failed)Constructed as Node node Node node = new Node(Thread.currentThread(), mode); 2.Quickly insert the current node to the end of the queue Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } 3.Join the team enq(node); 4.Returns the current node return node; }
The FIFO in AQS adopts lazy loading mode. Before the node joins the queue, the tail and head are always null. Therefore, pred here will be null and enq method will be called to join the queue:
private Node enq(final Node node) { 1.Spin until the node joins the team successfully for (;;) { 2.Get tail node Node t = tail; 3. Queue is null,Initialize queue if (t == null) { // Must initialize 3-1. Set sentinel node if (compareAndSetHead(new Node())) tail = head; } else { 4.Queue initialized 4-1.Set the predecessor node of the current node to tail node.prev = t; 4-2.CAS Set the current node as the new tail node if (compareAndSetTail(t, node)) { 4-3. Set the subsequent nodes of the original tail node as the current node t.next = node; 4-4. Return to original tail node return t; } } } }
Enter the enq method, because the queue has not been initialized, enter the 3-1 branch, set the sentinel node, and then enter the second cycle, set the current node as a new tail node and return to the original tail node.
Review the acquire method again:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
At this time, the t2 thread has been put into the blocking queue. Next, the acquirequeueueueued method will be called. At this time, the whole queue is as follows:
The acquirequeueueueued method is as follows, and its input parameter is the newly added t2 thread node:
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; 1.Spin, only when the lock is obtained successfully or interrupted can the loop exit for (;;) { 2.Get the precursor node of the current node final Node p = node.predecessor(); 3.If the predecessor node of the current node is the head node, an attempt is made to acquire the lock if (p == head && tryAcquire(arg)) { 3-1.If the lock is obtained successfully, set the current node as the new head node setHead(node); p.next = null; // help GC failed = false; return interrupted; } 4.If the precursor node is not the head node or the lock acquisition fails, confirm whether to park()own if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //park()own interrupted = true; } } finally { //If the current future is cancelled due to interruption or timeout, if (failed) cancelAcquire(node); } }
According to the acquirequeueueued method, the precursor node of t2 node is indeed the head node, but because t1 thread holds the lock, all tryAcquire methods must fail. At this time, shouldParkAfterFailedAcquire method should be called:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { 1.Get precursor node status int ws = pred.waitStatus; 2.If the precursor node status is SIGNAL,Then return true,Represents that the thread can be blocked if (ws == Node.SIGNAL) return true; 3.All nodes in cancel status before deleting the node from the queue if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { 4.Set the precursor node status to SIGNAL compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } 5.return false,Indicates that the thread cannot be blocked return false; }
Analyze shouldParkAfterFailedAcquire method. According to the queue diagram drawn above, if the waitStatus of pred node is 0, CAS method will be called to set waitStatus to SIGNAL, i.e. - 1, and return false. The new queue is shown in the figure:
This is the second cycle of the acquirequeueueueueueueued method. Since the t1 thread still holds the lock, it will still enter the shouldParkAfterFailedAcquire method. However, at this time, the waitStatus of the pred Node is already SIGNAL, so it will directly return true. Here we can see that when the waitState of the Node in AQS is SIGNAL,
It means that the subsequent drive node is blocked due to the failure to compete for mutually exclusive resources.
After shouldParkAfterFailedAcquire returns true, the parkAndCheckInterrupt method will be executed:
private final boolean parkAndCheckInterrupt() { 1.jdk The provided blocking thread method can only be called unpark Method or interrupted to wake up LockSupport.park(this); 2.Return thread interrupt status return Thread.interrupted(); }
In the parkAndCheckInterrupt method, we can see that after the t2 thread fails to compete for mutually exclusive resources, it first enters the AQS waiting queue and sets the state of the precursor node. At this time, it will call the park method to block itself until it is awakened.
So when will it be awakened? Regardless of interruption, the thread holding the lock can only wake up the thread in the waiting queue after releasing the lock.
③ Suppose that t3 thread calls the locking method of ReentrantLock at this time:
The locking process of thread t3 is the same as that of thread t2. It will not be introduced in detail here, but only the state diagram of the waiting queue in the final AQS
(2) Unlock
Suppose that at this time, the t1 thread calls the release lock method of ReentrantLock.
public void unlock() { sync.release(1); }
The unlock method directly calls the release method of Sync, a subclass of AQS. The release method is defined in the AQS class.
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 release method first calls the tryRelease method, which is defined in the Sync class.
protected final boolean tryRelease(int releases) {
1. calculation state Final value int c = getState() - releases;
2. Verify whether the current thread is a locked thread if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false;
3.state 0 indicates that the lock has been released and the waiting thread can be awakened if (c == 0) { free = true; setExclusiveOwnerThread(null); }
4.modify state setState(c); return free; }
The tryRelease method is mainly used to modify the state variable. If the state is 0 after modification, it returns true; otherwise, it returns false.
Because the t1 thread is locked only once, the state value is 1. After calling the tryrease method, the state is 0 and returns true. At this time, the unparksuccess method is called, which is defined in AQS.
private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ 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 unparksuccess method is mainly used to wake up the successor node of the head node. If the successor node has been cancelled, start from the end of the queue to find the wake-up node closest to the head of the queue.
Call locksupport After the unpark method wakes up the specified thread, the whole process of releasing the lock is over.
Summary of main processes for releasing locks:
1. Modify AQS state
2. Wake up the successor node of the head node in the waiting queue. If the successor node is cancelled, wake up the thread corresponding to the node closest to the head of the queue that meets the requirements from the back to the front.
Here, the t2 thread is awakened. Review the process when t2 is locked. t2 is blocked in the parkAndCheckInterrupt method. After being awakened, continue to execute the spin method.
Because the precursor node of t2 thread is the head node, t2 thread calls the tryAcquire method. Because t1 thread has released the lock, t2 thread can lock successfully, and then set itself as the head node. At this time, the whole locking and unlocking process is completely completed.
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; 1.Spin, only when the lock is obtained successfully or interrupted can the loop exit for (;;) { 2.Get the precursor node of the current node final Node p = node.predecessor(); 3.If the predecessor node of the current node is the head node, an attempt is made to acquire the lock if (p == head && tryAcquire(arg)) { 3-1.If the lock is obtained successfully, set the current node as the new head node setHead(node); p.next = null; // help GC failed = false; return interrupted; } 4.If the precursor node is not the head node or the lock acquisition fails, confirm whether to park()own if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //park()own interrupted = true; } } finally { //If the current future is cancelled due to interruption or timeout, if (failed) cancelAcquire(node); } }