Reentrantlock source code analysis

Friendly tip: the length is long, you can view the overall directory structure in the directory on the right
In this paper, the implementation of the lock interface class ReentrantLock to do a source code analysis, hoping to help you better understand the principle of ReentrantLock lock lock unlock. The use of ReentrantLock is not the focus of this article, but this article analyzes ReentrantLock from the source point of view, which can help you better understand ReentrantLock, know its nature, know its reason, and what we should have as technical people.

jdk version: 1.8

I. Introduction

ReentrantLock is under the java.util.concurrent.locks package, which implements the Lock interface and the Serializable interface.
ReentrantLock is a reentrant lock and mutually exclusive lock. It has the same basic behavior and semantics as the synchronized keyword with implicit monitor, but it has more methods and functions than synchronized.

Two. Example

As for the fair lock and unfair lock of ReentrantLock, here is an example of unfair lock. You can run it. If it is a fair lock, just change the parameter when creating the lock object to true

public class MyFairLock extends Thread{

    private ReentrantLock lock = new ReentrantLock(false);
    public void fairLock(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()  + "Holding lock");
        }finally {
            System.out.println(Thread.currentThread().getName()  + "Release the lock.");
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        MyFairLock myFairLock = new MyFairLock();
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() + "start-up");
            myFairLock.fairLock();
        };
        Thread[] thread = new Thread[10];
        for(int i = 0;i < 10;i++){
            thread[i] = new Thread(runnable);
        }
        for(int i = 0;i < 10;i++){
            thread[i].start();
        }
    }
}

3, Structure introduction

1. internal class

First, let's look at the overall structure of this class. We can see that I circle three internal classes in the figure, namely, Sync, NonefairSync, FairSync,
These three classes are the three core categories. We can see the internal methods of these three classes from the figure
Then let's take a look at the inheritance structure of these three classes, which must be kept in mind here. These classes and their parent classes will be mentioned later in the source code process

1.1 Sync

1.2 NonfairSync

1.3 FairSync

1.4 summary

As you can see, sync is the parent class of FairSync (fair lock implementation related class) and NonfairSync (unfair lock implementation related class), and sync inherits the AbstractQueuedSynchronizer class (AQS for short). AQS is not only useful in ReentrantLock, but also in Semaphore, CountDownLatch and FutureTask. I'm looking at this article A brief introduction to AbstractQueuedSynchronizer , interested partners can find out below

2. Constructor

ReentrantLock is a reentrant lock, and it supports fair lock and unfair lock
The following are the parameterless and parameterless constructors of ReentrantLock. From the function, we can see that if it is a parameterless construction, the default is an unfair lock. If it is a parameterless constructor, if our parameter is true, then it is a fair lock. If it is false, then it is an unfair lock
About the fair lock and unfair lock of ReentrantLock, let's talk about them before we parse the source code:

  • Fair lock can ensure that: the old thread queues to use the lock, and the new thread still queues to use the lock.
  • Unfair lock guarantee: the old thread queues to use the lock; however, it cannot guarantee that the new thread preempts the lock of the already queued thread. That is to say, threads only occupy when they join, which is unfair for the old threads. After they grab their own position, they can queue up in the same position. Next, they may bully you when other new threads join, which is why even the unfair lock uses the synchronous queue

4, Source code explanation (unfair lock)

Because the unfair lock is more complex than the fair lock, we mainly talk about the unfair lock, most of which may be explained by comments on the code bid winning
**Friendly tip: * * you can download a jdk source code and put the source code into your ide so that you can read the source code and understand it more easily with the explanation, and you can use the shortcut key to directly enter the method to view the class and specific content. For beginners, it is very complex to see the source code, so it is very recommended to do so, so that you will not get lost later. In addition, it is recommended to first It may be better to scan the code in the code block and start to see my comments.

1. lock up

1.1 locked access

In order to prevent people from looking at each other and not knowing which class they entered, they will mark which class they are in at the change place,
ReentrantLock class:

sync is the initialization when we call the constructor. Let's talk about unfair locks. It's not easy to annotate pictures. Stick code instead,

1.2 ReentrantLock NonfairSync inner class:

ReentrantLock.java:
/**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
       		 /*Call the method in AQS and set the state in AQS to 1 in cas mode,
            AQS In this scenario, ReentrantLock is used to indicate the number of times the owner thread has repeatedly acquired the lock (the number of levels of reentry),
            A value of 0 indicates that there is no thread occupation. If the value is set to 1 successfully, the lock is acquired successfully. If the value is not set to 1 successfully, false will not enter the code,
            Description state is not 0, only when state is 0 will it be changed to 1, and return true, which is the use of cas*/
            if (compareAndSetState(0, 1))
            	/*After obtaining the lock successfully, set the thread owner as the current thread. The next method is the method in the AbstractOwnableSynchronizer class, which is responsible for
            	Manage the threads that currently own the lock.*/
                setExclusiveOwnerThread(Thread.currentThread());
            else
            	//If the lock is not acquired successfully, this method will be explained below
                acquire(1);
        }

		//Next, this is the hook method. Just remember here. Next, you will use the cquire method
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

1.3 AbstractQueuedSynchronizer acquire() method

Then we will go on to explain the acquire method, which is the core part of the lock adding process. So we will give a general introduction first, and then give a detailed introduction to each method. We will enter the method discovery, which is the parent of AQS:

AbstractQueuedSynchronizer.java:
	public final void acquire(int arg) {
		/*In fact, the tryAcquire method at the bottom also exists in this class, but when it is called, it is actually a subclass of the call, because it is about
		The unfair lock is called the tryAcqure method in the upper code block. Of course, the fair lock has its own
		tryAcuire Method, the method called in the parent class is implemented by subclass, and this method is also called hook.
		Method, try to acquire the lock again*/
        if (!tryAcquire(arg) &&
        	//If it still fails to acquire the lock (tryAcquire returns false), according to the & & short circuit principle, the current thread will be added to the synchronization queue to wait under execution
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            //Block the current thread after joining the synchronization queue
            selfInterrupt();
    }

Then we will introduce the above methods in detail

1.3.1 the internal class of reentrantlock is the non fairtryacquire method of Sync

? Don't you think we should talk about the tryAcquire method? What the hell is this? You can go back to the ReentrantLock NonfairSync internal class through the sidebar directory. The tryAcquire method is the internal class. There is only one method in this method, that is to call the Sync nonfairTryAcquire method, the parent class of the NonfairSync class. If you don't say much, go to the code (OK, enough has been said It's just to be as detailed as possible and make everyone less confused):

//The acquires parameter is the number of locked reentry levels, but here we consider it as 1, and it is also 1 when the method is called
final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //state here is the number of reenters of the current lock. If it is 0, no thread holds the lock. Then try to acquire the lock
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            /*If c, that is, state is not 0, it means that the existing thread owns the lock. We can judge whether the thread owning the lock is the current thread,
            If yes, it means that the thread has re entered the lock (multiple times), then add acquire to the state and update the re-entry times*/
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            //If other threads are occupying the lock, the code in the upper method block is not executed, and the lock acquisition fails
            return false;
        }
1.3.2 addWaiter method of abstractqueuedsynchronizer

The lower method adds the current thread (included in the lower Node) that has not acquired the lock to the synchronization queue

1.3.2.1 Node inner class of abstractqueuedsynchronizer


Let's first introduce node, which is the internal class of AbstractQueuedSynchronizer. We mainly know the following variables here. Of course, we can skip them here. Later, we will look back when we use the source code:

  • int waitStatus:
    The node status field has the following statuses:
    * CANCELLED
    The value is 1. Scenario: when the thread waits for timeout or is interrupted and needs to cancel waiting from the synchronization queue, the thread is set to 1,
    It is cancelled (here the thread is in wait state before canceling). When the node is in the cancel state, it will not change;
    * SIGNAL
    The value is -1. Scenario: the subsequent node is in the waiting state. If the current node's thread releases the synchronization state or is cancelled
    (the current node status is set to - 1), the successor node will be notified so that the thread of the successor node can run;
    * CONDITION
    The value is -2. Scenario: the node is in the waiting queue, and the node thread is waiting on the Condition, when other threads are on the Condition
    After the signal() method is called, the node transfers from the waiting queue to the synchronization queue and joins in the acquisition of the synchronization state;
    * PROPAGATE
    The value is -3. Scenario: indicates that the next sharing state will be propagated unconditionally;
    * INITIAL
    Value is 0, the initial state of the node.
  • Node prev
    *The predecessor node is set when the node joins the synchronization queue (added at the end)
  • Node next
    *Successor node
  • Node nextWaiter
    *Wait for the successor node of the node. If the current node is SHARED, this field is a SHARED constant, that is, the node
    Type (exclusive and shared) and subsequent nodes in the wait queue share A single field. (Note: for example, the current node A is shared, then
    Then its field is shared, that is to say, in the waiting queue, the successor node of node A is also shared. If node A
    If it is not SHARED, its nextWaiter is not a SHARED constant, that is, exclusive.
  • Thread thread
    *Thread getting synchronization status
1.3.2.2 addWaiter method
private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        //Get the tail node to the current queue
        Node pred = tail;
        //If the tail node is not empty
        if (pred != null) {
        	//Set the predecessor node of the newly added node as the tail node
            node.prev = pred;
            /*Use cas mode to set tail variable (variable in AbstractQueuedSynchronizer) as the newly added node,
            Ensure that the tail node is always the tail node. Only when the original value is pred, the update succeeds*/
            if (compareAndSetTail(pred, node)) {
            	//Set the successor node of the previous tail node as the newly added node, that is, the newly added node is successfully placed behind the previous tail node and becomes a new tail node
                pred.next = node;
                return node;
            }
        }
        /*In fact, the method below already contains the content above, but why do you want to put it on the top? In fact, the reason why we add
        This part of "repeated code" is the same as "repeated code" when trying to acquire lock, and it deals with some special cases in advance, at the expense of certain code
        Readability in exchange for performance improvements.*/
        enq(node);
        return node;
    }

Then let's talk about the enq() method in the upper code block, which is also of the AbstractQueuedSynchronizer class:

1.3.2.2.1 enq() method
private Node enq(final Node node) {
		/*Next, this for is equivalent to an infinite loop, until the return is executed, which ensures all gains
		The thread that failed to take lock can finally join the synchronization queue after failing to retry*/
        for (;;) {
        	//t points to the tail node. If the queue is empty, it is empty
            Node t = tail;
            if (t == null) { //If the queue is empty / / 1 (used to discuss the problem under these ordinals)
            	/*CAS The head pointer is updated by. Only when the original value is null, the update succeeds. Here, it is just an empty node,
            	It is not updated to the newly added node we passed, because the node where the current thread is located cannot be directly inserted into the empty queue and blocked
            	The thread of is awakened by the predecessor node. Therefore, first insert a node as the first element of the queue, which wakes up when the lock is released
            	Logically, the first element of the queue can also represent the thread that is currently acquiring the lock, although it is not necessarily true
            	Hold its thread instance, and then the queue is not empty. Once again, the for loop can execute the next code block to add the incoming new node*/
                if (compareAndSetHead(new Node()))		//2
                    tail = head;						//3
            } else {
            	//If the tail node is not empty, the lower logic is as fast as the upper code, and it will not be repeated here
                node.prev = t;							//4
                if (compareAndSetTail(t, node)) {		//5
                    t.next = node;						//6
                    return t;
                }
            }
        }
    }

The whole process of joining the team is not complicated. It is a typical optimistic lock strategy of CAS plus failure retry. Only the update head pointer and update tail pointer are synchronized in CAS. It can be predicted that the performance is very good in the high concurrency scenario. But in the spirit of doubt, we can't help thinking about the real thread safety?

  • 1. When the queue is empty:
    Because the queue is empty, head=tail=null. If thread 2 is executed successfully, then before thread 3 is executed, because tail=null, other threads entering the method will fail in 2 places continuously because head is not null, so 3 will not have thread safety problems even if there is no synchronization.
  • 2. When the queue is not empty:
    Assuming that the thread executes 5 successfully, the operation of 4 must be correct at this time (prev pointer of the current node does point to the end node of the queue, in other words, tail pointer does not change, otherwise 5 will fail to execute). Because 4 succeeds, the order of the current node in the queue has been determined, so when 6 executes will not have any impact on thread safety,

Is it thread safe after 4 and 5?
4 put the back of 5. If the tail node is changed by other threads just after 5 is executed (that is to say, other threads finish executing t.next = node;), and the tail node variable here is still the original one. There are other thread nodes behind the original tail node, so we can imagine the error

1.3.3 acquirequed method of abstractqueuedsynchronizer

After the addWaiter method is finished, we will review the code structure of directory 1.3. Next, we will talk about acquirequeueued method

Upper Code:

/*The official explanation of this method: obtain the existing threads in the queue in exclusive and uninterrupted mode. Used for conditional wait method and get.
In short, it means waiting until the predecessor node of the thread node is the head node and obtains the lock.
The next node is our newly added thread node. arg is the parameter passed from the initial lock method call. Fair lock and
 The unfair lock sent 1*/
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            /*A dead loop. Normally, only when the thread gets the lock, it will jump out of the loop and the method will end*/
            for (;;) {
            	//Get the predecessor node of thread node
                final Node p = node.predecessor();
                //If the precursor node is the head node, and the lock is acquired successfully, the content in the code box will be executed. Set the current node as the head node, and the method ends
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //Make sure the state of the predecessor node is SIGNAL, and then block the current thread. Here's how
                if (shouldParkAfterFailedAcquire(p, node) &&  //Determine whether to block the current thread
                    parkAndCheckInterrupt())  //Block the current thread until it is awakened by the predecessor node
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

There are two if's in the upper loop. In the first if clause, the current thread will first determine whether the precursor node is the head node. If it is, it will try to acquire the lock. If the lock is acquired successfully, it will set the current node as the head node (update the head pointer). Why do we have to have the front node as the head node to try to acquire the lock? Because the header node represents the thread currently occupying the lock. Normally, when the thread releases the lock, it will notify the blocked thread in the subsequent node. After the blocked thread is awakened, it will acquire the lock. This is what we hope to see. However, there is another situation, that is, the predecessor node cancels the wait, and the current thread will also be awakened. At this time, it should not acquire the lock, but go back to find a node without canceling the wait, and then connect itself behind it. Once we successfully acquire the lock and set ourselves as the head node, we will jump out of the for loop. Otherwise, the second if clause will be executed: ensure that the state of the predecessor node is SIGNAL, and then block the current thread.
Moreover, there is a tryAcquire method in the upper code for loop. One of the advantages is that if the lock is acquired successfully when the loop is re executed, the cost of thread blocking and wake-up is saved, which is also an optimization in high concurrency scenarios

1.3.3.1 the shouldParkAfterFailedAcquire method of abstractqueuedsynchronizer

See the Node inner class of abstractqueuedsynchronizer in 1.3.2.1

/*The purpose of this method is to ensure that the state of the predecessor node of the current node is signal, which means that the thread will wake up after releasing the lock
 Thread blocked later. After all, the current thread can only be safely blocked if it is guaranteed to wake up.*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        //If the status of the precursor node is signal, return true to block the current node
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
		//If the status is CANCELLED, trace back to the queue head until a node whose status is not CANCELLED is found, and hang the current node node behind this node.
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
            	//The lower side is executed from back to front
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
        /*pred The status of is initialization. At this time, use the compareAndSetWaitStatus(pred, ws, Node.SIGNAL) method
        Change the state of pred to SIGNAL.*/
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

That is to say, the upper side will block the subsequent methods immediately only after the predecessor node has been in the signal state, corresponding to the first case above. In the other two cases, it is ensured that the precursor node is signal after execution, return false, and then re execute the loop in 1.3.3.

1.3.3.2 parkAndCheckInterrupt method of abstractqueuedsynchronizer

It can be seen from 1.3.3 that after 1.3.3.1 is executed, if it returns true, the next method will be executed. This method is used to block the thread, block the current thread, and wait for being awakened by the predecessor node, and return true after waking up; otherwise, continue infinite loop

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

After executing the acquire method in 1.3, we can complete the locking process.

2. unlock

2.1 unlocking the entrance

ReentrantLock class:

    public void unlock() {
        sync.release(1);//sync inherits AbstractQueuedSynchronizer, so the next block method is called
    }

AbstractQueuedSynchronizer class:

    public final boolean release(int arg) {
        if (tryRelease(arg)) {	//Release the lock (state-1). If the lock can be acquired by other threads after release (state=0), return true. 2.1.1 explanation
            Node h = head;
            if (h != null && h.waitStatus != 0)//The header node is not empty and is not in initialization state 0
                unparkSuccessor(h);		//Wake up the blocked threads in the queue, 2.1.2 explanation
            return true;
        }
        return false;
    }
2.1.1 tryRelease method of reentrantlock sync inner class

This is called by the parent class above, so it's still a hook method

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;		//Update state value
            if (Thread.currentThread() != getExclusiveOwnerThread()) //Throw an exception if the current thread is not a locked thread
                throw new IllegalMonitorStateException();
            boolean free = false; 
            if (c == 0) {
                free = true;	//If the updated state is 0, it means no reentry, which means the lock can be acquired by other threads
                setExclusiveOwnerThread(null);//Empty lock holding state
            }
            setState(c);	//Update state
            return free;
        }

2.1.2 unparkSuccessor method of abstractqueuedsynchronizer
    /**
     * Wakes up node's successor, if one exists.
     *
     * @param node the node
     */
    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); //Set header node to initialization state

        /*
         * 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) {	//If the successor node is empty or the wait is cancelled (1)
            s = null;
            //Traverse from the back to the front, and find the thread node closest to the head node to cancel waiting
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);  //Wake up a successor node thread without canceling the wait
    }

This thread has been successfully unlocked.

Published 44 original articles, won praise 12, visited 5349
Private letter follow

Tags: Java JDK less

Posted on Mon, 13 Jan 2020 10:13:39 -0500 by johnny44