The meaning of hasQueuedPredecessors method and head node of ReentrantLock class

Posted by ram4nd on Mon, 29 Jun 2020 09:57:37 +0200

Part of the inspiration comes from the article Java Concurrent Programming -- Lock

PART 1

1. If h==t is true, h and T are both null or the same specific node, and there is no successor node. false is returned.
2. If h!=t holds, head.next Whether it is null. If it is null, it returns true. When h!=t and h.next==null?? , may appear the first time other threads are queued. See enq method of AQS, compareAndSetHead(node) is completed, and when tail=head statement has not been executed, tail=null,head=newNode,head.next=null .
3. If h!=t holds, head.next != null, then judge head.next Whether it is the current thread. If it is, it returns false, otherwise it returns true (the head node is the node that obtained the lock, but at any time, the head node may occupy the lock or release the lock (unlock()), which is not blocked head.next It is necessary for the thread corresponding to the node to try to acquire the lock at any time)

1 public final boolean hasQueuedPredecessors() {
2     Node t = tail; 
3     Node h = head;
4     Node s;
5     return h != t &&
6         ((s = h.next) == null || s.thread != Thread.currentThread());
7 }

 

PART 2 explains why to judge: s.thread= Thread.currentThread ()

The question on the third floor of the comment area almost made me think that I misunderstood and wrote wrong here. Now it's December, and the article was written in April, which is almost forgotten... I read the article and source code carefully, and found that the article was not written in detail. There was a problem in writing, and some details were missed, so I want to add.   ---20191217

1,

According to the unlocking process of ReentrantLock, that is, the following four methods, we can see that after the thread releases the lock, it will still be in the head node of the queue, but the subsequent wake-up nodes of the head will be unpark ed
That is to say, at any time, the head node may occupy the lock (except that when enq() is executed for the first time, the head is only a new Node(), and does not actually correspond to any thread, but it "implicitly" corresponds to the first thread that obtained the lock but not queued, which is consistent with the meaning of the subsequent head), and may also release the lock (unlock()), Not blocked head.next It is necessary for the thread corresponding to the node to try to acquire the lock at any time

1 public void unlock() {
2     sync.release(1);
3 }

 

2,

Try to release the lock. After the lock is released successfully head.next Wake up from blocking

From here and in the following 3 and 4, we can see that although the thread has released the lock (state is set to 0), it does not point the head to the next node in the linked list (i.e. do a head like operation)= head.next Operation of)

Here is the corresponding point 1. If you can see it here, you can directly see point 5 for a long time

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 }

 

3,

Put state-1
When state=0, set exclusive ownerthread to null, indicating that the thread has released 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);
11     return free;
12 }

 

4,

hold head.next Point to the next node with waitstatus < = 0 and wake the node from blocking

 1 private void unparkSuccessor(Node node) {
 2     int ws = node.waitStatus;
 3     if (ws < 0)
 4         compareAndSetWaitStatus(node, ws, 0);
 5 
 6     Node s = node.next;
 7     if (s == null || s.waitStatus > 0) {
 8         // I don't understand why we have to start from tail Reverse node traversal?
 9         // Not from head.next Is it faster for nodes to start traversing?
10         s = null;
11         for (Node t = tail; t != null && t != node; t = t.prev)
12             if (t.waitStatus <= 0)
13                 s = t;
14     }
15     if (s != null)
16         LockSupport.unpark(s.thread);
17 }

 

5,

You need to know a little in advance: the hasQueuedPredecessors() method has only been called and executed in the tryAcquire() method, and the return of hasQueuedPredecessors() is false, indicating that you want to try to acquire the lock

The thread locking process is: lock() - >. Acquire() - > tryacquire ()

Here we first assume A scenario: thread A acquires the lock, and then thread B tries to acquire the lock but fails to obtain it. At this time, the head of the linked list is used for thread A, head.next Corresponding to B thread

When thread B fails to obtain the lock in the tryAcquire() in line 2, thread B will be added to the waiting list through the addWaiter() method in line 3, and then park will enter the waiting state in the acquiquequeued() method in line 3 and parkAndCheckInterrupt() in line 38

After thread A releases the lock, thread B will wake up again from line 38 and enter the for(;;;) loop. When thread B executes to line 28, that is, tryAcquire() is executed again, then hasQueuedPredecessors() and s.thread! Will be executed successively= Thread.currentThread (). As can be seen from the previous article, the head still points to thread A, head.next In other words, s points to the B thread and is also the current thread, so s.thread= Thread.currentThread () is false, i.e. you need to try to acquire the lock at this time (repeat this sentence again: unblocked head.next It is necessary for the thread corresponding to the node to try to acquire the lock at any time.

When thread B finally obtains the lock, it will point the head to the corresponding linked list node of thread B at line 30.

 1 public final void acquire(int arg) {
 2     if (!tryAcquire(arg) &&
 3         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
 4         selfInterrupt();
 5 }
 6 
 7 protected final boolean tryAcquire(int acquires) {
 8     // Omitting parts is not important
 9     
10     if (c == 0) {
11         if (!hasQueuedPredecessors() &&
12             compareAndSetState(0, acquires)) {
13             setExclusiveOwnerThread(current);
14             return true;
15         }
16     }
17     
18     // Omitting parts is not important
19 }
20 
21 final boolean acquireQueued(final Node node, int arg) {
22     boolean failed = true;
23     try {
24         boolean interrupted = false;
25         for (;;) {
26             final Node p = node.predecessor();
27             // Here it goes again tryAcquire
28             if (p == head && tryAcquire(arg)) {
29                 // hold head Points to the current node
30                 setHead(node);
31                 p.next = null; // help GC
32                 failed = false;
33                 return interrupted;
34             }
35             // If the lock cannot be obtained, it will enter the thread waiting state here
36             // If you wake up later, you will come out of here and continue for loop
37             if (shouldParkAfterFailedAcquire(p, node) &&
38                 parkAndCheckInterrupt())
39                 interrupted = true;
40         }
41     } finally {
42         if (failed)
43             cancelAcquire(node);
44     }
45 }

Topics: Java Programming