Unlocking the Cornerstone of Java Concurrent Programming: In-depth Exploration of the Mysteries of AQS

Introduction to Lock

I have learned through an in-depth analysis of the synchronized keyword in Javasynchronized . Let's learn another important implementation of concurrent programming thread synchronization Lock.

LockThe interface defines a set of methods for controlling thread access to shared resources, as follows: 

Compared with the synchronization  Synchronized lock that requires the JVM to acquire and release the lock implicitly, Lock the synchronization lock (hereinafter referred to as  Lock the lock) needs to acquire and release the lock explicitly, which provides more flexibility for acquiring and releasing the lock. Lock The basic operation of the lock is implemented through optimistic locking, but since  Lock the lock is also suspended when blocked, it is still a pessimistic lock. We can simply compare the next two synchronization locks through a picture to understand their respective characteristics:

Lockis an interface that only provides abstract methods for releasing and preempting locks, and the following specific implementations are provided in JUC.

  • ReentrantLock, a reentrant lock, belongs to the exclusive lock type, and has synchronizedsimilar functions.
  • ReentrantReadWriteLock(RRW), reentrant read-write lock, two locks are maintained in this class, one ReadLockis, and the other is WriteLockthat they respectively implement Lockthe interface.
  • StampedLock, a new locking mechanism introduced in Java 8, which is ReentrantReadWriteLockan improved version of .

AbstractQueuedSynchronizer(AQS)They are all implemented by dependent  classes.

Introduction to AQS

AQS is an abstract class, which mainly maintains the value (state) of a resource synchronization state variable and a CLH two-way queue for storing queuing threads . At the same time, the mechanism of thread blocking waiting and lock allocation when awakened. The specific structure is as follows:

AQSUse a volatilemember variable of int type to represent the synchronization state, complete the queuing work of resource acquisition threads through the built-in first-in-first-out CLHqueue, and encapsulate each thread that wants to seize resources into a Node node to realize the allocation of locks. The modification of the State value is completed through CAS.

CLH lock is actually a kind of spin fair lock based on logical queue non-thread starvation. It is named CLH lock because it is the invention of three masters, Craig, Landin and Hagersten.

AQS has an internal class Node, the Node node encapsulates each thread waiting to acquire resources, which includes the thread itself and its waiting state that needs to be synchronized, such as whether it is blocked, whether it is waiting to wake up, whether it has been canceled, etc.

Here it can be seen intuitively that the node node will store the currently blocked request resource thread, and the variable waitStatus indicates the waiting status of the current Node node. There are five values ​​as follows:

  • CANCELLED (1): Indicates that the current node has canceled scheduling. When timeout or interrupted (in the case of interrupted response), it will trigger a change to this state, and the node after entering this state will no longer change.
  • SIGNAL (-1): Indicates that the successor node is waiting for the current node to wake up. When the successor node joins the queue, the status of the predecessor node will be updated to SIGNAL.
  • CONDITION (-2): Indicates that the node is waiting on the Condition. When other threads call the signal() method of the Condition, the node in the CONDITION state will waiting queue to the synchronization queue , waiting to acquire the synchronization lock.
  • PROPAGATE (-3): In shared mode, the predecessor node will not only wake up its successor node, but may also wake up the successor node.
  • 0 : The default state when new nodes are enqueued.

The key methods of AQS are as follows:

  1. acquire(): It is used for the thread to obtain the status of the synchronizer. If it cannot be obtained, the thread will enter the blocked state and wait for other threads to release the synchronizer.
  2. release(): Used for the thread to release the state of the synchronizer and wake up other threads in the waiting queue.
  3. tryAcquire(int): Exclusive mode. Attempts to fetch the resource, returning true on success and false on failure.
  4. tryRelease(int): Exclusive mode. Attempts to release the resource, returning true on success and false on failure.
  5. tryAcquireShared(int): sharing method. Try to fetch resources. A negative number indicates failure; 0 indicates success, but no resources are left; a positive number indicates success, with resources left.
  6. tryReleaseShared(int): sharing method. Attempts to release the resource, and returns true if subsequent waiting nodes are allowed to wake up after release, otherwise returns false.

Analysis of AQS implementation through ReentrantLock

ReentrantLockIt is an important and commonly used synchronizer in JUC's concurrent package, and its bottom layer is realized by relying on AQS. Below we will look at ReentrantLockthe implementation in detail.

Acquire lock process

ReentrantLock preemptive lock interaction diagram:

ReentrantLockSyncNofairSyncAQSlocklockacquiretryAcquirenofairTryAcquiretrue/falseJudging the success of the preemption lock and the failure of the preemption addWaiterReentrantLockSyncNofairSyncAQS

The overall process is as follows:

Get lock source code analysis

Let's take a look at the source code of these methods:

Open the method of the inner class that lock.lock()jumps to at this time ReentrantLockNonfairSync(NonfairSync继承自AQS)lock()

acquire

acquire()The method is the method in AQS, here is that the thread is unsuccessful in locking and occupying resources, and the thread is placed in the CLH queue to wait for the notification to wake up the core entrance

 The core process steps are as follows:

  1. tryAcquire() tries to acquire resources directly, and returns directly if it succeeds ( here reflects the unfair lock, each thread will try to preempt and block once when acquiring the lock, and there may be other threads waiting in the CLH queue );
  2. addWaiter() adds the thread to the tail of the waiting queue and marks it as exclusive mode;
  3. acquireQueued() causes the thread to block in the waiting queue to acquire resources, and returns only after the resources are acquired. Returns true if interrupted during the entire waiting process, otherwise returns false.
  4. If the thread is interrupted while waiting, it is unresponsive. Self-interruption selfInterrupt() is only performed after obtaining resources to make up for the interruption.

tryAcquire

The implementation of FairSync.tryAcquire is as follows:

 
 

java

copy code

final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }

This code is the implementation of the nonfairTryAcquire method in AbstractQueuedSynchronizer (AQS). This method is used to unfairly attempt to obtain the state of a synchronizer.

  1. First, get the current thread and get the current state value c.
  2. If the current state value c is 0, it means that the synchronizer is not currently occupied by other threads. In this case, directly use the compareAndSetState method to modify the state from 0 to acquires (the expected value is 0, and the update value is acquires). If the modification is successful, it means that the current thread has successfully acquired the synchronizer, and the current thread is set as the owner of the exclusive lock, and then returns true.
  3. If the current state value c is not 0, it means that the synchronizer has been occupied by other threads. At this time, it is necessary to judge whether the current thread is the exclusive lock owner of the synchronizer. If so, add acquires to the current state value (increase the number of times the lock is held), and return true.
  4. If none of the above conditions are met, it means that the current thread cannot obtain the status of the synchronizer, and returns false.

addWaiter

This method is used to add the current thread to the end of the waiting queue CLH and return the node where the current thread is located

 
 

java

copy code

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; }

  1. First, create a new node Node that contains the current thread and the specified wait mode mode.
  2. Attempts to add new nodes to the queue using the fast path. First get the tail node pred of the current queue, if the tail node is not null, point the prev of the new node to pred, and try to use the compareAndSetTail method to update the tail node to the new node. If the update is successful, point the next of the original tail node pred to the new node and return the new node.
  3. If the fast path fails, that is, the tail node is null or compareAndSetTail fails, it means that other threads are modifying the queue concurrently. At this time, the new node needs to be added to the queue using a complete enqueue operation (enq).
  4. In the enq method, new nodes are first added to the tail of the queue by a spin operation.
  5. Return the new node.

The function of this method is to add the current thread as a waiting thread to the synchronization queue. It employs an optimistic strategy, first trying to add new nodes to the tail of the queue using the fast path, and failing that a full enqueue operation is used. In this way, competition and thread blocking can be avoided in most cases, and concurrency performance can be improved.

acquireQueued

This method is used to acquire the lock in the synchronization queue. When the lock cannot be acquired directly, the current thread is added to the waiting queue and spins and waits.

 
 

This

copy code

final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }

  1. First, set a flag bit failed to true to record whether an exception occurs during the process of acquiring the lock.
  2. Loop in a try-catch-finally block until the lock is successfully acquired or interrupted.
  3. In the loop, first obtain the predecessor node p of the current node, and judge whether the predecessor node of the current node is the head node and try to acquire the lock successfully (by calling the tryAcquire method). If so, set the current node as the new head node, disconnect the original head node from the current node, set the next pointer of the predecessor node to null, and finally set the flag bit failed to false, and return to interrupted status interrupted.
  4. If the precursor node p does not meet the conditions, call the shouldParkAfterFailedAcquire method to determine whether the current thread needs to be blocked, and call the parkAndCheckInterrupt method to block the thread and check whether the thread is interrupted. If interrupted, set the interrupted state interrupted to true.
  5. Loop back to step 2 and continue trying to acquire locks or block.
  6. If the lock still cannot be acquired at the end of the loop, that is, an exception occurs during the process of acquiring the lock, call the cancelAcquire method to cancel the acquisition operation of the current node.

shouldParkAfterFailedAcquire

This method is used to determine whether the current thread should be blocked after the lock acquisition fails:

 
 

This

copy code

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) return true; if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }

  1. First, obtain the waitStatus of the precursor node pred.
  2. If the waiting status waitStatus is equal to Node.SIGNAL, it means that the predecessor node has set the status to release the lock to send a signal, so the current node can safely block and return true.
  3. If the wait status waitStatus is greater than 0, it means that the predecessor node has been cancelled. Loop through the skipped predecessor node and the canceled node before it, until a predecessor node pred whose wait state is less than or equal to 0 is found. Then update the prev pointer of the current node to the found predecessor node pred, and point the next pointer of the predecessor node pred to the current node node.
  4. If the wait status waitStatus is 0 or Node.PROPAGATE, it indicates that a signal is needed to wake up the current node, but it does not block immediately. Call the compareAndSetWaitStatus method to change the waiting state of the predecessor node pred to Node.SIGNAL to indicate that a signal is needed.
  5. Return false, indicating that the current thread does not need to be blocked.

Unlock source code analysis

unlock

NonfairSync(NonfairSync继承自AQS)method unlock():

 
 

java

copy code

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

release

 
 

java

copy code

public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }

tryRelease

This method is used to release lock resources.

 
 

java

copy code

protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }

  1. First, obtain the state value c = getState() - releases of the current lock, indicating the new state after the lock is to be released.

  2. Determine whether the current thread is the owner of the exclusive lock, and throw an IllegalMonitorStateException if not.

  3. Process according to the new state c:

    • If the new state c is equal to 0, it means that the lock is fully released. Set exclusiveOwnerThread to null, indicating that there is currently no owner, and set the free flag to true.
    • Otherwise, the state of the update lock is the new state c.
  4. Returns the free flag indicating whether the lock has been fully released.

unparkSuccessor

If the release of resources above returns true successfully, then unparkSuccessor()the next thread in the waiting queue will be executed to wake up

 
 

scss

copy code

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); /* * 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) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }

  1. First, judge the waiting state ws of the node, if ws is less than 0, then try to set it to 0. This is to pre-clear state that may require a signal. Even if this operation fails or the waiting thread changes state, subsequent operations will not be affected.
  2. Get the successor node s of the node, usually the successor node is the next node of the current node. But if the successor node is canceled or empty, it traverses forward from the tail to find the successor node that has not been cancelled. This is to find the nodes that really need to be woken up.
  3. If the successor node s that needs to be woken up is found, call the LockSupport.unpark(s.thread) method to wake up the thread corresponding to the node.

Fair Lock & Unfair Lock

ReentrantLockIt is divided into fair lock and unfair lock. The default construction method is unfair lock. If we need to build a fair lock, we only need to pass the parameter true:

 
 

java

copy code

Lock lock = new ReentrantLock(true);

Its locking and unlocking process is almost the same as the unfair lock above, with some differences in some details:

 
 

This

copy code

protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }

Here, lock()there is no use of CAS to try to lock and occupy resources like unfair locks. It is a direct call acquire(1)to enter the queue. There is more logic of hasQueuedPredecessors in locking

 
 

java

copy code

public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());

Guess you like

Origin blog.csdn.net/BASK2312/article/details/131305744