1. Create context
Why introduce ReentranLock when Java already has a built-in lock called synchronized?
Synchronized is based on the JVM built-in lock, through the internal object Monitor (Monitor lock), based on the entry and exit of Monitor object implementation method and code block synchronization, the implementation of Monitor lock depends on the underlying operating system Mutex lock (Mutex lock) implementation. Blocked threads will be suspended and wait for rescheduling, resulting in switching between user and kernel states, which has a significant impact on performance.
With this background in mind, Doug Lea provides api-level mutex ReentranLock in JDK1.5, which implements reentrant, breakable, fair locking, and more. Before synchronization optimization, there was still a gap between synchronized performance and ReentranLock performance.
2. Easy to use
public class ReentrantLockSimple {
private ReentrantLock lock = new ReentrantLock();
public void syncIncr(a) {
try {
lock.lock();
// Todo simulates services without releasing locks
Thread.sleep(10 _000);
} catch (Exception e) {
e.printStackTrace();
} finally{ lock.unlock(); }}public static void main(String[] args) {
ReentrantLockSimple simple = new ReentrantLockSimple();
// Simulate concurrent scenarios
new Thread(simple::syncIncr).start();
newThread(simple::syncIncr).start(); }}Copy the code
3. Source code analysis
3.1 already structure
First, the RentrantLock object structure, have a general understanding.
public class ReentrantLock implements Lock.java.io.Serializable {
private final Sync sync;
// Sync controller (AQS)
abstract static class Sync extends AbstractQueuedSynchronizer {... }// Unfair lock (implementation)
static final class NonfairSync extends Sync {... }// Fair lock (implementation)
static final class FairSync extends Sync {... }// Constructor: default unfair lock
public ReentrantLock(a) {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : newNonfairSync(); }}Copy the code
3.2 lock lock. The lock ()
In fact, all the methods in ReentrantLock are done by the member object Sync.
So, let’s go straight to nonfairsync.lock ()
final void lock(a) {
// CAS sets the status value. If successful, the current thread is set to the exclusive thread, which means that the lock has been acquired
if (compareAndSetState(0.1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// AbstractQueuedSynchronizer.java
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
Copy the code
Next, analyze the three key methods in Acquire () :
- TryAcquire () : Attempts to acquire a lock, overridden by subclasses (where reentrant, fair lock features are implemented);
- AddWaiter () : AQS maintains a linked list that wraps the current thread as a Node and joins the queue;
- AcquireQueued () : Acquires a thread already in the queue in exclusive uninterruptible mode.
3.2.1 tryAcquire
final boolean nonfairTryAcquire(int acquires) {
// The current thread
final Thread current = Thread.currentThread();
// Get the status of the current lock
int c = getState();
if (c == 0) {
// Fair locking must also be satisfied in the following if! Hasqueuedtoraise (), literally no one waiting in a queue.
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true; }}// If the current thread has acquired the lock, repeat the lock ~
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
Copy the code
The current method returns true whether the lock is acquired or repeated. Acquire () will terminate without any further enlistment operations.
The nature of unfair locking is reflected in this method (in fact, the lock method is also directly acquire lock, more direct).
3.2.2 addWaiter
Notice that the current method is called with a single parameter: Node.exclusive. This parameter actually acts as an identifier and has two types: exclusive and shared.
private Node addWaiter(Node mode) {
// Wrap the current thread as a Node
Node node = new Node(Thread.currentThread(), mode);
// The current Node is added to the tail of the queue and is referenced to the previous tail
Node pred = tail;
if(pred ! =null) {
node.prev = pred;
// CAS sets the current Node to the end of the queue
if (compareAndSetTail(pred, node)) {
pred.next = node;
returnnode; }}// Loop until you join the team
enq(node);
return node;
}
private Node enq(final Node node) {
// loop until return
for (;;) {
Node t = tail;
// create a new Node with head and tail in the queue
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
// CAS sets the end of the queue
if (compareAndSetTail(t, node)) {
t.next = node;
returnt; }}}}Copy the code
AddWaiter () simply wraps the current thread as a Node, sets it to the end of the queue (if necessary, initializes the queue), and guarantees queue entry in an infinite loop.
Below, a general idea about the structure of the Node (AbstractQueuedSynchronizer inner classes)
static final class Node {
/* * Thread waiting state (initialized to 0) : * CANCELLED (1) : indicates that the current node has been CANCELLED; * SIGNAL (-1) : indicates that the current node is waiting to be awakened unparking; * CONDITION (-2) : indicates that the current node needs to meet certain conditions; PROPAGATE (-3) : PROPAGATE */
volatile int waitStatus;
// Front and rear drive nodes: standard linked list structure
volatile Node prev;
volatile Node next;
// The current thread is held
volatile Thread thread;
}
Copy the code
3.2.3 acquireQueued
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
// Whether the thread is interrupted
boolean interrupted = false;
// This loop will normally only execute three times for any one thread
for (;;) {
// Get the precursor node
final Node p = node.predecessor();
// If the front node is the head of the queue, try to acquire the lock
if (p == head && tryAcquire(arg)) {
// The current node is set as the queue head
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
interrupted = true; }}}finally {
if(failed) cancelAcquire(node); }}Copy the code
At this point, the node (current thread) has been successfully enqueued. If the current node is the first candidate, an attempt is made to acquire the lock. But the lock may not have been released (state! If the lock fails to be acquired, the current thread will be blocked.
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// Note that this is the wait state of the precursor node
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/* * If the state of the precursor node is signal notification, it means that the current node can safely block */
return true;
if (ws > 0) {
/* * If the state of the precursor node is canceled, the state of the precursor node is not greater than 0 *
do {
/** * the following line is equivalent to: * pred = pred.prev; * node.prev = pred; * /
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/* * the status value must be 0 or -3 (PROPAGATE), indicating that the current node is the */ that needs to be awakened
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
Copy the code
As you can see, there is a section of code in this method that is too simple.
The red block’s pred and N1 states are both cancelled (with a status value of 1).
First, the do block is executed to point pred to N1 when it is found that the wait state of PREd is cancelled when the if judgment is performed. Execute the while judgment and find that the state of N1 node is also cancelled. Execute the do block again and point pred to N2 node. Execute the while to determine that the N2 node state is not canceled, and the loop ends. Before the end of the loop, the node’s precursor pointer already points to N2, and after the loop ends, N2’s back pointer points to Node, thus establishing a bidirectional association.
If you are careful, you must have noticed that the acquireQueued() method will only be executed three times, so why three times?
Let’s say the lock lock has been acquired by some thread, and the synchronization block is still being executed, without time to release the lock. At this point, thread A also executes the business method, and then attempts to acquire the lock, but inevitably fails to acquire the lock and performs the enqueue operation. At this point, the Node wrapped by the current thread occupies the end of the queue, and the head of the queue is an “empty” Node initialized. The pred argument is actually the head node (with a state value of 0). The first time through the loop, execute the else logic and set the pred state to SIGNAL. Note that false is returned. This will cause the if judgment of the current method to end. In the second loop, the first if is valid and returns true, parkAndCheckInterrupt() is executed to block the thread; Until the thread is woken up, open the third loop, obtain the lock success, directly back, end the loop.
Note: as described above, there are no cancelled nodes in the queue and the lock is not contested, so it is the general case.
private final boolean parkAndCheckInterrupt(a) {
LockSupport.park(this);
return Thread.interrupted();
}
Copy the code
Have to say, AQS method name or very appropriate, basically see the name know meaning. Block the current thread and return the thread interrupt status!
Locksupport.park (this)
- Block execution of the current thread without releasing lock resources held by the current thread.
- Can be awakened by another thread calling the locksupport.unpark () method;
- The underlying call to Unsafe’s native method.
// java.util.concurrent.locks.LockSupport
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false.0L); // A breakpoint is set, debug executes the sample code, and the thread is woken up 10 seconds later
setBlocker(t, null);
}
Copy the code
3.2.4 summary
ReentranLock a set of lock process summed up, is to try to obtain the lock, obtain success, update the lock state, set the exclusive thread; The current thread will be wrapped as a Node Node and added to the end of a linked list maintained by AQS. Finally, the current thread will be blocked until it is woken up and attempts to acquire the lock again (unfair lock, there is a possibility of being grabbed).
3.3 unlock the lock, unlock ()
How does a self-blocking thread wake up when locking, and what is the trigger mechanism?
Next, let’s walk through another important component of ReentranLock step by step.
// ReentranLock
public void unlock(a) {
sync.release(1);
}
// AbstractQueuedSynchronizer
public final boolean release(int arg) {
// Try to release the lock
if (tryRelease(arg)) {
Node h = head;
if(h ! =null&& h.waitStatus ! =0)
// Wake up the waiting thread
unparkSuccessor(h);
return true;
}
return false;
}
Copy the code
The unlocking logic is divided into two parts: release the lock and wake up the blocked thread.
3.3.1 tryRelease
protected final boolean tryRelease(int releases) {
// Number of remaining locks after this unlocking
int c = getState() - releases;
// If the unlocked thread is not an exclusive thread, throw an exception
if(Thread.currentThread() ! = getExclusiveOwnerThread())throw new IllegalMonitorStateException();
boolean free = false;
// Release the lock and empty the exclusive thread
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// Update lock times
setState(c);
return free;
}
Copy the code
It echoes back and forth. Reentrant features!
Note that the lock and unlock times must be consistent, otherwise the waiting thread in the queue cannot be woken up.
3.3.2 rainfall distribution on 10-12 unparkSuccessor
private void unparkSuccessor(Node node) {
/* * If the state is negative (i.e., a signal may be needed), try to clear the expected signal */
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// Pre-wake node (head)
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);
}
Copy the code
The pre-awakened node S is the rear drive node (FIFO) of node.
But if S is empty or cancelled, you need to find a normal node from the queue. How do I find it? Starting from the node at the end of the queue, we traverse forward, and finally S points to the nearest normal node in the queue.
The thread held by the S node is then woken up by calling locksupport.unpark ().
At this point, ReentranLock a complete set of lock unlocking process analysis is completed ~