ReentrantLock is a reentrant mutex provided in Java packages that has the same effects as synchronized but better performance than synchronized and is used in many highly concurrent programming applications. Because most of the students only stay in the API call level, to the principle of ReentrantLock, or even know nothing, so WRITE this article, let the students really ReentrantLock to take down!

This article will be shared from the following aspects:

  • Usage scenarios
  • The source code to achieve
  • Design idea

Usage scenarios

public class ReentrantLockTest {
    private ReentrantLock lock = new ReentrantLock();

    public void method(a) {
        lock.lock();
        // do somethinglock.unlock(); }}Copy the code

To use ReentrantLock, call Lock () before synchronizing a block of code and unlock() after. Also note that lock() and unlock() must occur in pairs. If a synchronized code block might throw an exception, you must place the unlock() call ina finally block.

The source code to achieve

Open lock() to see its implementation.

public void lock(a) {
    sync.lock();
}
Copy the code

It does this by calling sync’s lock() method. Let’s take a look at sync’s definition.

private final ReentrantLock.Sync sync;
Copy the code

Sync is an inner class, so let’s take a look at its lock() implementation.

abstract void lock(a);
Copy the code

Obviously, a subclass inherits Sync, so we can look at the Sync initialization code to see which subclass is used to initialize Sync.

public ReentrantLock(a) {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
Copy the code

There are two subclasses for sync initialization, FairSync and NonfairSync. These are known as fair locks and unfair locks. ReentrantLock uses an unfair lock by default, but it can also be specified when ReentrantLock is created.

Let’s take a look at the implementation of NonfairSync to lock().

final void lock(a) {
    // This step is a quick attempt to obtain the lock operation, fair lock does not exist
    if (this.compareAndSetState(0.1)) {
        this.setExclusiveOwnerThread(Thread.currentThread());
    } else {
        this.acquire(1); }}Copy the code

If you look at the code, an unfair lock starts with a call to compareAndSetState() in an attempt to acquire the lock, which is unfair to other threads already waiting in the lock queue. Okay, guys, this is an important distinction between fair locks and unfair locks.

Now let’s look at the implementation of compareAndSetState().

protected final boolean compareAndSetState(int var1, int var2) {
    return unsafe.compareAndSwapInt(this, stateOffset, var1, var2);
}
Copy the code

The principle is to update the value of state via CAS atomic operations provided by UNSAFE. Also found compareAndSetState () is located in AbstractQueuedSynchronizer in the class, and then found that the Sync inherited AbstractQueuedSynchronizer, We need to update the state also lies in AbstractQueuedSynchronizer.

private volatile int state;
Copy the code

State records the number of times the lock is re-entered. If it is 0, it means that no thread currently holds the lock. In this case, a CAS operation can be used to quickly complete the lock application, which is a quick attempt.

When fast try after failure, will call acquire () method, acquire () is from AbstractQueuedSynchronizer, we look at the code.

public final void acquire(int var1) {
    if (!this.tryAcquire(var1) && this.acquireQueued(this.addWaiter(Node.EXCLUSIVE), var1)) { selfInterrupt(); }}Copy the code

TryAcquire () first, if you are a lock ii (lock reentrant), then you will have a great time.

To see how an unfair lock is acquired, open tryAcquire().

protected final boolean tryAcquire(int var1) {
    return this.nonfairTryAcquire(var1);
}
Copy the code

Continue looking at nonfairTryAcquire() in Sync.

final boolean nonfairTryAcquire(int var1) {
    // Get the current thread
    Thread var2 = Thread.currentThread();
    // Get the number of locks
    int var3 = this.getState();
    // The lock has not yet been occupied by any thread, so try locking quickly
    if (var3 == 0) {
        if (this.compareAndSetState(0, var1)) {
            this.setExclusiveOwnerThread(var2);
            return true; }}// The current thread is the second generation of the lock, state + 1
    else if (var2 == this.getExclusiveOwnerThread()) {
        int var4 = var3 + var1;
        // Lock reentrant cannot exceed ingteger. MAX_VALUE, otherwise it will explode
        if (var4 < 0) {
            throw new Error("Maximum lock count exceeded");
        }
        this.setState(var4);
        return true;
    }
	// There is no entrepreneurial success, nor lock the second generation, only the fate of failure
    return false;
}
Copy the code

After watching the nonfairTryAcquire() operation, we know how lock reentrant with unfair locks is played.

If the thread doesn’t get the lock, it will have to wait in the queue for the lock, which is called the addWaiter() method. Let’s see how this works.

private Node addWaiter(Node mode) {
    // Construct a Node that is bound to the current thread. Mode passes node. EXCLUSIVE, which represents the EXCLUSIVE lock
    Node var2 = new Node(Thread.currentThread(), mode);
    // Get the end node of the queue
    Node var3 = this.tail;
    // The end node is not empty, CAS quickly tries to update itself as the end node
    if(var3 ! =null) {
        var2.prev = var3;
        if (this.compareAndSetTail(var3, var2)) {
            var3.next = var2;
            returnvar2; }}// The end node does not exist, or updating the end node failed
    this.enq(var2);
    return var2;
}
Copy the code

When the end node is null, or when updating the end node fails, enq() is called for processing.

private Node enq(Node var1) {
    // Note that while(true) does not stop until the destination is reached
    while(true) {
        Node var2 = this.tail;
        // The end node is empty, which means that the entire queue is empty. The head node does not exist
        if (var2 == null) {
            // Update the head node through CAS. From this line of code, we can also know that the head node in the lock queue is empty and not bound to any thread
            if (this.compareAndSetHead(new Node())) {
                // The head node and the end node are the same
                this.tail = this.head; }}// The end node already exists, so CAS updates itself as the end node
        else {
            var1.prev = var2;
            if (this.compareAndSetTail(var2, var1)) {
                var2.next = var1;
                returnvar2; }}}}Copy the code

The use of while(true) ensures that the current thread is added to the end of the lock queue when enq() returns.

AcquireQueued () is called after the Node corresponding to the current thread joins the end of the queue. Let’s see what this method does.

final boolean acquireQueued(Node var1, int var2) {
    // Identifies whether the current thread has acquired the lock when the method returns, with the default value true indicating that it did not
    boolean var3 = true;

    try {
        // Indicate whether the current thread has been woken up while sleeping
        boolean var4 = false;
		// Spin the lock
        while(true) {
        	// Get the last node of the current node
            Node var5 = var1.predecessor();
            // If the last node is the head node, you can try to grab the lock directly
            if (var5 == this.head && this.tryAcquire(var2)) {
                // Set yourself as the head node
                this.setHead(var1);
                // Remove the dependency of the previous header so that it can be killed by GC
                var5.next = null;
                // I have successfully grabbed the lock
                var3 = false;
                // If the current thread was woken up when it went to sleep
                boolean var6 = var4;
                return var6;
            }
			// The current node is not behind the head node, or is behind the head node, but the lock grab failed
            / / call shouldParkAfterFailedAcquire (), to find a home to return to (make up after a node to inform yourself), then you can call parkAndCheckInterrupt () to go to sleep
            if (shouldParkAfterFailedAcquire(var5, var1) && this.parkAndCheckInterrupt()) {					
                // Wake up unexpectedly while sleeping, note that you are also a man who has had an interruption
                var4 = true; }}}finally {
        // If var3 is true, the thread did not acquire the lock and it is invalidated, so cancelAcquire() must be called before the method exits
        if (var3) {
            this.cancelAcquire(var1); }}}Copy the code

Once a thread is added to the queue, it spins wildly to do these things: get woken up, go to sleep, get woken up, and keep going until it gets the lock and leaves.

As for how to look for a person to wake up his own thread, we see shouldParkAfterFailedAcquire () implementation.

// var0 is the previous node, var1 is the current node
private static boolean shouldParkAfterFailedAcquire(Node var0, Node var1) {
    int var2 = var0.waitStatus;
    // If the previous node meets the condition of being woken up, it means that the previous node will grab the lock sooner or later. When the lock is used up, it will automatically notify itself
    if (var2 == -1) {
        return true;
    } else {
        // The last node gave up the lock, so I can't count on it
        if (var2 > 0) {
            do {
                var1.prev = var0 = var0.prev;
            } while(var0.waitStatus > 0);
			
            var0.next = var1;
        } else {	// waitStatus is not greater than 0, CAS sets it to -1 (satisfying the wakeup criteria), but the setting may not succeed
            compareAndSetWaitStatus(var0, var2, -1);
        }
		// I can't find someone to wake me up
        return false; }}Copy the code

Since sleep is not, that still continue to see to have to rob lock qualification, have to rob a, so the cycle of the cycle.

At some point, the thread finds someone to wake it up, and it can go to sleep, which is naturally done by calling parkAndCheckInterrupt().

private final boolean parkAndCheckInterrupt(a) {
    Unsafe.park (); // Unsafe.park (); // unsafe.park()
    LockSupport.park(this);
    // The thread wakes up, but it needs to determine if any interruptions occurred during sleep
    return Thread.interrupted();
}
Copy the code

ParkAndCheckInterrupt () returns true if an interrupt occurred. Here we go again to the acquire() method, which executes selfInterrupt().

static void selfInterrupt(a) {
    // The thread is marked with interrupt bits, which are delayed but never absent
    Thread.currentThread().interrupt();
}
Copy the code

Okay, guys, I’m done here with lock(). Now let’s move on and see how unlock() works.

public void unlock(a) {
    this.sync.release(1);
}
Copy the code

It looks like we’re calling AQS release(), so let’s see.

public final boolean release(int var1) {
    // Release the lock, return true only if state is 0
    if (this.tryRelease(var1)) {
        Node var2 = this.head;
        // The head node is not empty, which means the queue is not empty, and waitStatus is not 0, which means it has a successor node, so it can wake up the next one to grab the lock
        if(var2 ! =null&& var2.waitStatus ! =0) {
            this.unparkSuccessor(var2);
        }
        return true;
    } else {
        return false; }}Copy the code

Call tryRelease() to release the lock and see how it works.

protected final boolean tryRelease(int var1) {
    // state subtracts 1
    int var2 = this.getState() - var1;
    // If the thread is not the thread of the current lock, it will throw an exception
    if(Thread.currentThread() ! =this.getExclusiveOwnerThread()) {
        throw new IllegalMonitorStateException();
    } else {
        // Indicate whether the lock has been fully released
        boolean var3 = false;
        // There are no threads holding the lock, so the next thread can hold the lock
        if (var2 == 0) {
            // Return true when the lock is fully released
            var3 = true;
            // Set the lock owner to null
            this.setExclusiveOwnerThread((Thread)null);
        }
		// Update the state value
        this.setState(var2);
        returnvar3; }}Copy the code

If the lock is fully released, the next person must be awakened to grab it. To find the next one, in particular, look at the unparksucceeded ().

private void unparkSuccessor(Node var1) {
    int var2 = var1.waitStatus;
    if (var2 < 0) {
        // Set the head node to its initial state
        compareAndSetWaitStatus(var1, var2, 0);
    }

    Node var3 = var1.next;
    if (var3 == null || var3.waitStatus > 0) {
        var3 = null;
		// Find the last node in the queue, and finally find the first node in the queue (except the head node)
        for(Node var4 = this.tail; var4 ! =null&& var4 ! = var1; var4 = var4.prev) {if (var4.waitStatus <= 0) { var3 = var4; }}}// Wake up the node
    if(var3 ! =null) { LockSupport.unpark(var3.thread); }}Copy the code

The tryLock() method is not fair. The tryLock() method is not fair. One of the differences between fair and unfair locks mentioned earlier is the different tryAcquire() implementation in lock(). Non-fair lock Any new thread can participate in the lock, but fair lock must be honest queue, pay attention to the first come first served, specific look below.

protected final boolean tryAcquire(int var1) {
    Thread var2 = Thread.currentThread();
    int var3 = this.getState();
    if (var3 == 0) {
        Hasqueuedtoraise () is crucial. It is the central manifestation of fairness
        if (!this.hasQueuedPredecessors() && this.compareAndSetState(0, var1)) {
            this.setExclusiveOwnerThread(var2);
            return true; }}else if (var2 == this.getExclusiveOwnerThread()) {
        / / reentrant lock
        int var4 = var3 + var1;
        if (var4 < 0) {
            throw new Error("Maximum lock count exceeded");
        }
        this.setState(var4);
        return true;
    }
	// Failed to grab the lock
    return false;
}
Copy the code

Hasqueuedtorathe function of a thread to qualify to steal a lock when one of two conditions is met: 1. The lock synchronization queue has only one node; 2. The second node belongs to the current thread.

Design idea

Take a look at the lock synchronization queue internally maintained by AQS.

ReentrantLock uses AQS to add unlock. AQS internally maintains a bidirectional linked list lock synchronization queue, and maintains the head node head, tail node and semaphore state. Each Node is a Node object with prev defined, next pointing to its upstream and downstream, and a waitStatus object representing the thread status (wait locked or abandoned). When a new thread needs to grab a lock, create a Node mapped to the thread and add it to the end of the lock synchronization queue. Of course, there is an important point here. It will make a judgment when joining. If the current end node is in the abandoned state, it will continue to traverse forward to find a reliable node as the upstream. If the state of AQS is 0, resources are not occupied. The CAS thread can update the state. If the state is updated successfully, the lock is successfully added. If state is not 0, it means the resource is occupied by the thread. If the occupant is you, you can re-enter. If the occupant is not you, you can wait.

This is the end of the ReentrantLock source code and principle analysis. There will also be more updates on Java and other dry materials, students must combine to read, complement each other, to form a complete knowledge system. Finally, like my article students, welcome to pay attention to the following public number, not missed any valuable dry goods.