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 () :

  1. TryAcquire () : Attempts to acquire a lock, overridden by subclasses (where reentrant, fair lock features are implemented);
  2. AddWaiter () : AQS maintains a linked list that wraps the current thread as a Node and joins the queue;
  3. 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 ~