AQS Series (7)-Final: AQS Summary

Foreword

This article is a summary of the previous AQS series of articles, first look at the following questions:

1. How is the reentrant feature of ReentrantLock and ReentrantReadWriteLock implemented?

2. Which variable controls whether the lock is occupied?

3. When multiple threads compete for an exclusive lock, how does the thread that does not grab the lock block?

4. Can reading and reading be shared forever without blocking?

Do you know the answer to the above questions? Are all the principles clear? If yes, there is no need to read this article, otherwise please read it slowly.

 

text

1. Realization of reentrancy (for exclusive lock)-Question 1

    Reentrancy is achieved through exclusiveOwnerThread and state. In the parent class AbstractOwnableSynchronizer of AQS, there is a member variable exclusiveOwnerThread to store the thread when the exclusive lock is acquired. The reentrant feature is realized by this variable. If it is judged from the state that the current lock is already occupied, then it is judged whether the current thread is stored in this variable. If it is, the lock is acquired and the lock count is +1, otherwise the lock cannot be acquired. (This is for exclusive locks, shared locks do not belong to this category)

2. The value of state in AQS-Question 2

    The state is a volatile int type, which is used to record the locked state and the number of reentries, but the recording method is different for different implementation classes. The following are respectively explained:

[ReentrantLock] : state is used to record the lock holding state and the number of reentries. State = 0 means no thread holds the lock; state = 1 means there is a thread holding the lock; state = N means exclusiveOwnerThread this thread has N reentries Got this lock.

[ReentrantReadWriteLock] : state is used to record the occupancy status of the read-write lock and the number of threads held (read lock), the number of reentries (write lock) , the high 16 bits of the state record the number of threads holding the read lock, and the low 16 bits The number of write lock thread reentries. If the 16-bit value is 0, it means that no thread occupies the lock, otherwise it means that a thread holds the lock. In addition, for read locks, the number of read locks acquired by each thread is recorded by HoldCounter in the local thread variable.

[Semaphore] : State is used for counting. state = N means that there are N semaphores that can be allocated, state = 0 means that there are no semaphores, and all threads that require acquire semaphores are waiting at this time

[CountDownLatch] : The state is also used for counting . Each countDown is decremented by one. When it is reduced to 0, the thread blocked by await is awakened.

 

3. About the value of waitStatus in Node

waitStatus is of volatile int type and has 5 types of values, whose functions are as follows:

1: CANCEL, that is, the canceled state, the node in this state is an invalid node, and it will be skipped directly during execution. Generally, this state will only appear in special abnormal scenarios;

0: Initialized state, all newly created Node nodes are in this state;

-1: SIGNAL, you can wake up. If a node fails to lock, you must enter the blocking state. You must first set its previous node to -1 before you enter the park state. Search for parkAndCheckInterrupt () in AQS to find that, As long as this method appears, there must be a shouldParkAfterFailedAcquire (p, node) method to set the waitStatus of p to -1;

-2: CONDITION, the state related to the conditional queue, not involved in ReentrantReadWriteLock and ReentrantLock;

-3: PROPAGATE, which can be propagated. I don't see any use for it.

 

4. About head node and tail node

When the first thread comes to acquire the lock, the queue will not be initialized, but the exclusiveOwnerThread variable will be changed to the current thread (in the case of exclusive lock), then head and tail are null;

The first thread is not finished, and the second thread comes again. At this time, the queue will be initialized, a new empty Node will be assigned to head and tail, and then the thread to be queued after tail (that is, head) (details (See the enq method below), at this time a two-way queue with two nodes is formed, head is new Node (), tail is the newly added Node to be queued; then if the lock is not obtained, the waitStatus of the head will be set to -1, suspend itself, waiting for unparkSuccessor (head) to wake up.

addWaiter method: If tail is not empty, assign the current node node to tail, and the original tail becomes the leading node of the new tail on the linked list. It can be seen that the new way of adding this linked list is the latter entry type. This also explains why when awakening the thread, we traverse the tail forward to find the node node that meets the criteria in front.

 1 private Node addWaiter(Node mode) {
 2         Node node = new Node(Thread.currentThread(), mode);
 3         // Try the fast path of enq; backup to full enq on failure
 4         Node pred = tail;
 5         if (pred != null) {
 6             node.prev = pred;
 7             if (compareAndSetTail(pred, node)) {
 8                 pred.next = node;
 9                 return node;
10             }
11         }
12         enq(node);
13         return node;
14     }

The enq method code in the AQS class is as follows: similar to addWaiter, except that there is one more step to initialize the head / tail.

 1 private Node enq(final Node node) {
 2         for (;;) {
 3             Node t = tail;
 4             if (t == null) { // Must initialize
 5                 if (compareAndSetHead (new Node ())) // Initialize the head node, then the head pointer points to the new Node () object
 6 tail = head; // The tail is also initialized together, at this time will continue to go through the for loop again
 7             } else { 8 node.prev = t; // First point the node's prev pointer to the local variable t; 9 if (compareAndSetTail (t, node)) {// Replace the object value of the memory address where tail is located with node 10 t.next = node; // Point the next pointer of tail to the node node, then the node node becomes the new tail 11 return t; // return the old tail t
12  } 13  } 14  } 15 }

 

4. Questions about conditional sharing of read-read locks-Question 4

In ReentrantReadWriteLock, read lock and read lock cannot always be performed at the same time. If the current operation is a read lock, and the first queue is a write lock, then a new read lock will enter the linked list and block instead of directly acquiring Read lock, because if this is not set, there may be a read lock that has been coming over, and the subsequent write lock is always in a blocked state and the lock cannot be obtained.

See the unfair read lock implementation of the readShouldBlock method -apparentlyFirstQueuedIsExclusive () method

1 final boolean apparentlyFirstQueuedIsExclusive() {
2         Node h, s;
3         return (h = head) != null &&
4             (s = h.next)  != null &&
5             !s.isShared()         &&
6             s.thread != null;
7     }

 

5. Object's wait / notify and LockSupport park / unpark methods-Question 3

LockSupport wakes up and suspends the thread based on Thread's direct semaphore, while wait / notify is object-based and needs to depend on the Monitor object.

The semantics of park / unpark is easier to understand. park is blocking for the current thread, and unpark is awakening for the specified thread, which is not contrary to common sense like wait / notify.

Unpark provides a license for the specified thread, and the thread can continue to execute with this permission; park is waiting for a license. Unpark can be executed before the park, the thread will not block. However, unpark cannot reenter the stack, and multiple unparks will also be used directly by one park.

At the bottom, UNSAFE's park / unpark method operates on a Paker object (each thread has a Parker instance). The object maintains a _counter variable. After the unpark method is called, the variable changes from 0 to 1, indicating that it has continued execution. Permit, return to 0 after execution, if the variable is not 1 after calling the park method, the thread will block until it becomes 1 and then wake up to continue execution.

 

I will write about this for the time being, and I will learn about other tools in the JUC package later.

Guess you like

Origin www.cnblogs.com/zzq6032010/p/12076690.html
AQS
AQS