"Java Concurrency Programming" 15, reentrantlock implementation of the principle of deep exploration

Source: Internet
Author: User
Tags cas volatile

The original text has been written in very detailed, directly to the great God's article forwarded over the https://www.cnblogs.com/xrq730/p/4979021.html

Objective

This article is classified in the Java Basic classification, in fact, there is really no foundation. There are many articles on the Internet that write the difference of Reentrantlock, Reentrantlock and synchronized, and there are few articles that study reentrantlock and can articulate the principles of reentrantlock, This article is to study the realization principle of reentrantlock. The study of the principle of reentrantlock need a better Java foundation and the ability to read code, some friends do not understand that it is OK, you can see later, I believe you will have some gains.

Finally say, Reentrantlock is based on AQS implementation, which will be mentioned below, Aqs is the basis of CAs, if not very familiar with CAS friends, you can look at this article unsafe and CAs.

Abstractqueuedsynchronizer

Reentrantlock implementation of the premise is Abstractqueuedsynchronizer, referred to as AQS, is the core of Java.util.concurrent, Countdownlatch, Futuretask, Semaphore, Reentrantlock, and so on have an inner class that is the subclass of this abstract class. Let's introduce the AQS in two tables. The first is node, because Aqs is based on FIFO queue implementation , so there must be nodes, node is a node, in which node is:

Properties Custom
Node SHARED = new Node () Indicates that node is in shared mode
Node EXCLUSIVE = null Indicates that node is in exclusive mode
int CANCELLED = 1 node is set to cancel status because of timeout or interruption, the canceled node should not go to competition lock, can only keep the cancellation state unchanged, cannot be converted to other State, node in this state will be kicked out of the queue, collected by GC
int SIGNAL =-1 Indicates that node's successor node is blocked and needs to be notified.
int CONDITION =-2 Indicates that node is in the conditional queue because it is blocked waiting for a condition
int PROPAGATE =-3 Use in Shared Mode header node may be in this state, indicating that the next acquisition of a lock can be transmitted unconditionally
int Waitstatus 0, new node will be in this state
Node prev The predecessor node of a node in the queue
Node Next The successor node of a node in the queue
Thread thread This node holds the thread that represents the thread waiting for the lock
Node Nextwaiter Represents the next node waiting for condition

After watching node, let's look at the variables and methods in Aqs:

Properties/Methods Meaning
Thread Exclusiveownerthread This is the property of the Aqs parent class Abstractownablesynchronizer, which represents the current owner of the exclusive mode Synchronizer
Node As already described above, the basic unit of the FIFO queue
Node Head Header node in FIFO queue
Node Tail The tail node in the FIFO queue
int state Sync status, 0 means unlocked
int GetState () Get synchronization Status
setState (int newstate) Set synchronization Status
Boolean compareandsetstate (int expect, int update) Using CAs to set state
Long Spinfortimeoutthreshold = 1000L Time of thread spin wait
Node Enq (final node node) Inserting a node into the FIFO queue
Node Addwaiter (node mode) Creates and expands a wait queue for the current thread and the specified pattern
void Sethead (Node node) Set the head node of the queue
void Unparksuccessor (Node node) Invoke node-held thread if it exists
void Doreleaseshared () Action to do a release lock in shared mode
void Cancelacquire (Node node) Attempt to cancel an in-progress node acquisition lock
Boolean Shouldparkafterfailedacquire (node pred, node node) Whether the current thread should be disabled after attempting to acquire a lock and wait for
void Selfinterrupt () Interrupts the current thread itself
Boolean parkandcheckinterrupt () Disables the current thread from entering the wait state and interrupting the thread itself
Boolean acquirequeued (Final node node, int arg) Threads in the queue acquire locks
Tryacquire (int arg) Attempt to acquire a lock ( implemented by Aqs subclasses )
tryrelease (int arg) Attempt to release the lock ( implemented by Aqs subclasses )
Isheldexclusively () Whether to hold the lock alone
Acquire (int arg) Get lock
Release (int arg) Release lock
Compareandsethead (Node Update) Using CAs to set head node
Compareandsettail (node expect, node update) Using CAs to set up tail node
Compareandsetwaitstatus (node node, int expect, int update) Use CAs to set the wait state in a node

Some of the most important methods and properties in Aqs are listed above. The entire AQS is a typical application of template patterns , designed to be very ingenious, for the FIFO queue of various operations in the AQS has been implemented,AQS subclasses generally only need to rewrite tryacquire (int arg) and tryrelease (int ARG) two methods .

The realization of Reentrantlock

There is an abstract class sync in Reentrantlock:

Private final sync sync;    /**     * Base of synchronization control for this lock. subclassed     * into fair and nonfair versions below. Uses AQS State to * represent the number of holds on the     lock.     */    abstract static class Sync extends Abstractqueuedsynchronizer {    ...
}

Reentrantlock the implementation class Fairsync and Nonfairsync of the sync based on the Boolean parameter of the incoming construction method, representing the Fair sync and the non-fair sync respectively. Because reentrantlock we use a lot of non-fair locks, so look at how the unfair lock is implemented. Assuming thread 1 calls the lock () method of Reentrantlock, thread 1 will have an exclusive lock and the entire call chain is simple:

The first thread to get a lock does two things:

1. Set the state of Abstractqueuedsynchronizer to 1

2. Set Abstractownablesynchronizer thread as current

When these two steps are done, it means that thread 1 has exclusive locks. Then thread 2 also try to get the same lock, thread 1 does not release the lock in the case of the inevitable is not feasible, so threads 2 will be blocked. So, how is thread 2 blocked? Look at the method call chain of thread 2, which is more complex:

Call chain see really very long, okay, combined with code analysis, in fact reentrantlock not so complicated, we just a little bit to strip code:

1 final void Lock () {2     if (compareandsetstate (0, 1)) 3         Setexclusiveownerthread (Thread.CurrentThread ()); 4     Else 5         acquire (1); 6}

The first thread 2 attempts to use the CAs to determine whether the state is 0, 0 is set to 1, of course, this step is definitely a failure, because thread 1 has set the state to 1, so the 2nd line must be false, so thread 2 goes to the 5th line of the Acquire method:

Public final void acquire (int arg) {    if (!tryacquire (ARG) &&        acquirequeued (Addwaiter (node.exclusive), ARG))        selfinterrupt ();}

Literally it is very good to understand the meaning of the IF, go first to determine the condition to try to obtain a lock, if the result is false that fails, go to the second judgment condition to add a FIFO waiting queue. So first look at what the Tryacquire method does, and this method eventually calls the sync's Nonfairtryacquire method:

1 Final boolean nonfairtryacquire (int acquires) {2     final Thread current = Thread.CurrentThread (); 3     int c = GETST Ate (); 4     if (c = = 0) {5         if (compareandsetstate (0, acquires)) {6             setexclusiveownerthread (current); 7             return Tru E 8         } 9     }10     else if (current = = Getexclusiveownerthread ()) {one         int nextc = c + acquires;12         if (next C < 0)//overflow13             throw new Error ("Maximum lock count Exceeded");         setState (NEXTC);         return true;     }17     return false;18}

Because state is volatile, state has visibility to thread 2, thread 2 Gets the latest state, and again determines if the lock can be held (the thread 1 synchronous code executes faster, the lock is released at the moment), and it cannot return false.

Note the 16th line of 10~, the function of this code is to let a thread can call the same reentrantlock multiple times, each call to state+1, because a thread has already held a lock, so there is no competition, so there is no need to use CAs to set the state ( Equivalent to a bias lock ). From this code can be seen, nextc each add 1, when nextc<0 throw error, then the same lock can re-enter the maximum integer.max_value times, that is, 2147483647.

Then go to the if of the second judgment inside, first go Aqs the Addwaiter method:

Private node Addwaiter (node mode) {    node node = new Node (Thread.CurrentThread (), mode);    Try the fast path of Enq; Backup to full Enq on Failure    Node pred = tail;    if (pred! = null) {        Node.prev = pred;        if (Compareandsettail (pred, node)) {            pred.next = node;            return node;        }    }    Enq (node);    return node;}

First create a node of the current thread, the mode is exclusive (because the incoming mode is a null), and then determine if there are no nodes on the queue, create a queue, so go Enq method:

1 Private node Enq (final node node) {2 for     (;;) {3         Node t = tail; 4         if (t = = null) {//Must initialize 5             node H = new node ();//Dummy header 6             h.next = Node; 7             Node.prev = h; 8             if (Compareandsethead (h)) {9                 tail = node;10                 return h;11             }12         }13         else {             Node.prev = t;15             if (compareandsettail (t, node)) {                 T.next = node;17                 return t;18}19         }20}21     }

This method actually draws a picture should be better understood, after forming a queue should be like this:

Each step is shown with a graph, because thread 2 is the node that is the first to wait, so there must be no content on the FIFO queue, tail is null, walking is the 4th line ~ 10th line of code logic. This uses the CAs to set the head node, of course, it is possible to thread 2 set the head node when the CPU switch, Thread 3 has set the head node to form a queue shown, then thread 2 again to get tail, because tail is volatile, so the thread 2 is visible, Thread 2 saw that tail was not NULL, went to the else inside 13 rows to add itself behind the tail node. The whole process came down and formed a two-way queue. Finally Walk Aqs's acquirequeued (node, 1):

1 Final boolean acquirequeued (final node node, int arg) {2     try {3         Boolean interrupted = false; 4 for         (;;) {5             final Node p = node.predecessor (); 6             if (p = = head && tryacquire (ARG)) {7                 Sethead (node); 8                 p. next = null; Help GC 9                 return interrupted;10             }11             if (Shouldparkafterfailedacquire (p, node) &&12                 Parkandcheckinterrupt ())                 interrupted = true;14         }15     } catch (RuntimeException ex) {         Cancelacquire (node); +         throw ex;18     }19}

At this point, because thread 2 is the true first node of the two-way queue (there is also a h), so line 5th ~ Line 10th again to determine if thread 2 can acquire the lock (maybe this time the internal process 1 has been executed to release the lock, state from 1 to 0), if still not, First Call Aqs's Shouldparkafterfailedacquire (p, node) method:

1 private static Boolean Shouldparkafterfailedacquire (node pred, node node) {2     int s = pred.waitstatus; 3     if (S & Lt  0) 4/*         5          * This node have already set status asking a release 6          * to signal it, so it can safely Park 7          */ 8         return true; 9     if (S > 0) {         /*11          * predecessor was cancelled. Skip over predecessors and12          * indicate retry.13          */14 do     {Node.prev     = pred = pred.prev;16     } w  Hile (Pred.waitstatus > 0);     pred.next = node;18}19 else20         /*21          * Indicate that we need a signal, But don ' t park yet. Caller22          * would need to retry to make sure it cannot acquire Before23          * parking.24          */25          COMPAREANDSETW Aitstatus (pred, 0, node.signal);     return false;27}

Spit The Groove First, the code format of the code is really bad (it seems that the development of the JDK is not well written), the Waitstatus is H's waitstatus, is obviously 0, So at this point the waitstatus of H is set to noed.signal that is-1 and returns false. Since the return of false, the above acquirequeued 11 lines if the natural not set, then go for the loop for the first time, or try to acquire the lock, unsuccessful, continue to walk shouldparkafterfailedacquire, at this time Waitstatus is 1, Less than 0, go to the third row of judgment, return true. Then go to Acquirequeued's 11 line if the second judging condition Parkandcheckinterrupt:

Private Final Boolean parkandcheckinterrupt () {    Locksupport.park (this);    return thread.interrupted ();}
public static void Park (Object blocker) {    Thread t = thread.currentthread ();    Setblocker (t, blocker);    Unsafe.park (False, 0L);    Setblocker (t, null);}

In the final step, the park method that calls Locksupport blocks the current thread. At this point, the full process of using Reentrantlock to let thread 1 monopolize the lock, thread 2 into the FIFO queue, and block is already sorted out.

After the operation of Lock () is clear, it is necessary to explore the unlock () when the code has done something, and then look at the next part.

What to do when unlock ()

Do not draw a flowchart, directly look at the code flow, relatively simple, call Reentrantlock Unlock method:

public void Unlock () {    sync.release (1);}

Take the release of Aqs:

1 Public Final Boolean release (int arg) {2     if (Tryrelease (ARG)) {3         Node h = head;4         if (h! = null && H. Waitstatus! = 0) 5            unparksuccessor (h); 6         return true;7     }8     return false;9}

First Call Sync's tryrelease attempt to release the lock:

1 Protected Final Boolean tryrelease (int releases) {2     int c = getState ()-releases; 3     if (thread.currentthread ()! = Getexclusiveownerthread ()) 4         throw new Illegalmonitorstateexception (); 5     Boolean free = false; 6     if (c = = 0) {7 Free         = true; 8         setexclusiveownerthread (NULL); 9     }10     setState (c);     return free;12}

First of all, only when the c==0 will let Free=true, this and the above one thread multiple calls to the lock method to accumulate state is corresponding to the number of times the lock () method must call the same number of unlock () method only, so that a lock to all untie.

When a thread is completely unlocked for the same reentrantlock, the state of Aqs is naturally 0, and Abstractownablesynchronizer's exclusiveownerthread is set to null. This means that no thread occupies the lock and the method returns True. The code continues down, the fourth line of the release method above, H is not null set, H's Waitstatus is-1, not equal to 0 is also established, so go to the 5th line of the Unparksuccessor method:

1 private void Unparksuccessor (node node) {2/     * 3      * Try to clear status in anticipation of signalling.  It is 4      * OK If the This fails or if status is changed by waiting thread. 5      *     /6 Compareandsetwaitstatus (node, Nod e.signal, 0); 7  8/     * 9      * Thread to Unpark are held in successor, which is Normally10      * just the next node.  But if cancelled or apparently null,11      * traverse backwards from tail to find the actual12      * non-cancelled Succe ssor.13      */14     Node s = node.next;15     if (s = = NULL | | s.waitstatus > 0) {         s = null;17 for         (Nod e t = tail; T! = null && t! = node; t = T.prev)             if (t.waitstatus <= 0) +                 s = t;20    }21     if (s! = null)         -Locksupport.unpark ( S.thread); 23}

S is the next node of H, this node inside thread is thread 2, because this node is not equal to NULL, so walk 21 lines, thread 2 is Unpark, to run. One important question is: How does the lock be solved to ensure that the entire FIFO queue is reduced by one node? This is a very ingenious design, and back to the Aqs of the Acquirequeued method:

1 Final boolean acquirequeued (final node node, int arg) {2     try {3         Boolean interrupted = false; 4 for         (;;) {5             final Node p = node.predecessor (); 6             if (p = = head && tryacquire (ARG)) {7                 Sethead (node); 8                 p. next = null; Help GC 9                 return interrupted;10             }11             if (Shouldparkafterfailedacquire (p, node) &&12                 Parkandcheckinterrupt ())                 interrupted = true;14         }15     } catch (RuntimeException ex) {         Cancelacquire (node); +         throw ex;18     }19}

The blocked thread 2 is blocked on line 12th, note that there is no return statement, that is, blocking the completion of thread 2 will still make a for loop . Then, the blocking is complete, the thread 2 node is the predecessor node is p, thread 2 tries to Tryacquire, and then thread 2 becomes the head node, the P's next is set to NULL, so that all objects in the original head node do not point to any block memory space, H belongs to the contents of the stack memory, and the method end is automatically recycled, so that as the method is called, the original header node does not have any references to it, so it is automatically recycled by the GC. At this point, a return statement is encountered, the Acquirequeued method ends, and the following node is the same principle.

Here's a detailed look at the Sethead method:

private void Sethead (node node) {    head = node;    Node.thread = null;    Node.prev = null;}

The precursor node inside the Sethead method is null and there are no threads, so why not use a waiting thread as head node?

Because a thread can be canceled at any time due to an interruption, and if it is canceled, node will naturally be GC, then the GC will have to head node's successor node into a new head and to deal with a variety of situations, it is very troublesome. Using a node with no thread is a guide, because head has no threads and will not be canceled.

Another look at the above Unparksuccessor 14 lines, is to prevent the head of the next node is canceled, so, from the end of the head traversal, find a node closest to the head, the node Unpark operation.

Reentrantlock implementation of other methods

If you can understand the implementation of the Reentrantlock, then you will find that the rest of the reentrantlock the implementation of some of the other methods is very simple, from the JDK API about the Reentrantlock method of introduction to this part, to give a few examples:

1, int getholdcount ()

Final int getholdcount () {    return isheldexclusively ()? GetState (): 0;}

The lock () method that gets reentrantlock is called several times, which is the current value of state

2. Thread GetOwner ()

Final Thread GetOwner () {    return getState () = = 0? Null:getexclusiveownerthread ();}

Gets the current owning lock thread, which is the value of Exclusiveownerthread in Abstractownablesynchronizer

3, collection<thread> getqueuedthreads ()

Public final collection<thread> Getqueuedthreads () {    arraylist<thread> list = new Arraylist<thread > ();    for (Node p = tail; P! = null; p = p.prev) {        Thread t = p.thread;        if (t! = null)            List.add (t);    }    return list;}

Traverse from the end of the tail and add into the ArrayList

4, int getqueuedlength ()

Public final int getqueuelength () {    int n = 0;    for (Node p = tail; P! = null; p = p.prev) {        if (p.thread! = null)            ++n;    }    return n;}

Iterate from the end of the tail and accumulate N. Of course, this method and the method above may be inaccurate, because it is possible for the other threads to add node to the end of the queue when traversing.

The rest of the methods are similar, you can go to see for yourself.

Legacy issues

The Reentrantlock process has been basically clear to the manager, and now there is a legacy: We know that Reentrantlock can specify a fair lock or a non-fair lock, so what exactly is the code difference resulting in fair locking and unfair lock generation ?

To tell the truth, this problem, I myself has not fully figured out yet. After that, we will continue to follow up on this issue, and once we understand it, we will update this article or a new post to specifically describe the differences in code between fair Reentrantlock and unfair Reentrantlock.

"Java Concurrency Programming" 15, reentrantlock implementation of the principle of deep exploration

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.