JUC concurrent programming -- Interpretation of AQS source code

Posted by matthew798 on Sat, 08 Jan 2022 13:08:11 +0100

catalogue

1. What is AQS

2. Use of LockSupport

3. Analyze AQS source code with ReentrantLock

Unfair locking operation

Release lock operation

1. What is AQS

AQS(AbstractQueuedSynchronizer) is a framework used to build locks and synchronizers. Using AQS can easily and efficiently construct a large number of synchronizers widely used, such as ReentrantLock and Semaphore, and other synchronizers such as ReentrantReadWriteLock, SynchronousQueue, FutureTask, etc. are based on AQS.

The core idea of AQS is that if the requested shared resource is idle, the thread requesting the resource is set as a valid worker thread, and the shared resource is set to the locked state. If the requested shared resources are occupied, a set of mechanisms for thread blocking and waiting and lock allocation when waking up are required. This mechanism AQS is implemented with CLH queue lock, that is, the thread that cannot obtain the lock temporarily is added to the queue.

CLH(Craig,Landin,and Hagersten) queue is a virtual two-way queue (virtual two-way queue, that is, there is no queue instance, but only the association relationship between nodes). AQS encapsulates each thread requesting shared resources into a node of a CLH lock queue to realize lock allocation.

2. Use of LockSupport

LockSupport is a tool class for thread blocking and wake-up provided by JUC. This tool allows threads to block and wake up at any position. The main methods are as follows

public static void park(Object blocker);//The current thread is blocked indefinitely, with a blocker object, which is used to determine the cause of thread blocking
public static void park();//Indefinitely blocking threads
public static void parkNanos(long nanos);//Blocking the current thread has a blocking time limit
public static void parkNanos(Object blocker, long nanos);//Blocking the current thread has a blocking object with a time limit
public static void parkUntil(Object blocker, long deadline);//Block the current thread until a certain time
public static void unpark(Thread thread);//Wake up a blocked thread. It will wake up only when the thread is blocked. If it is not blocked, no operation will be performed.

LockSuport easy to use

 public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("The thread is blocked....... one");
            LockSupport.park();
            System.out.println("The thread is awakened....... two");
        }, "t1");
        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //Prepare to wake up the thread
        System.out.println("Ready to wake up the thread......... three");
        LockSupport.unpark(t1);
    }

The results of the above execution are 1, 3 and 2. When the thread is blocked, the thread will continue to execute only by calling unpark to wake up the thread.

LockSupport.park() and thread Sleep() difference

  • Thread.sleep() cannot wake up from the outside. It can only wait for itself to wake up or an exception occurs. Locksupport Park() can be accessed through locksupport The unpark () method wakes up.
  • Thread.sleep() declares an InterruptedException interrupt exception, and locksupport The park () method does not need to catch exceptions
  • And thread Compared with sleep(), locksupport The park () method blocks and wakes the specified thread more accurately and flexibly.
  • Thread.sleep() itself is a native method, locksupport Park () is a static method, but the underlying layer calls the native() method of the Unsafe class.
  • Locksupport.park() allows you to set a Blocker object, which is mainly used to provide monitoring tools or diagnostic tools to determine the cause of thread blocking.
  • Thread.sleep() and locksupport Neither Park () will release the lock it holds.

LockSupport.park() and object Wait() difference

  • Object.wait() needs to be used with the synchronized keyword, and locksuport Park () can be executed anywhere.
  • Object.wait() also needs to throw an interrupt exception. LockSupport.park() is not required.
  • If the thread executes notify() without wait(), it will throw Java Lang.illegalmonitorstateexception exception, but locksupport Unpark () does not throw any exceptions.
  • Object.wait() releases the lock and locksupport Park () does not release the lock.

3. Analyze AQS source code with ReentrantLock

This analysis is based on JDK17, which is different from jdk8 code, but the idea is the same.

ReentrantLock has the following internal classes

Abstract static class sync extensions abstractqueuedsynchronizer {} / / inherits AQS

Static final class nonfairsync extensions {} / / the key to implementing unfair locks

Static final class fairsync extensions {} / / the key to realizing fair lock

Unfair locking operation

The class diagram structure of NonfairSync is as follows

The following methods will be executed to decrypt the lock step by step.

Interpretation of initialTryLock() method

 

  1. The code at 1 changes the state value state from 0 to 1 through CAS operation. If the modification is successful, set the current thread to exclusive state, and then return true. Because the lock() method is (! initialTryLock()), it directly indicates that the lock is obtained successfully.
  2. The code at 2 indicates that the current thread is set to exclusive state.
  3. The code at 3 determines whether the current thread already holds the lock. If it is the same, it means re-entry of the lock. Then, increase the state value state by 1 and return true to indicate success in obtaining the lock.
  4. Interpretation of acquire(1) method

Interpretation of tryAcquire(1) method

This method indicates that if the lock is not obtained, the thread will not be blocked directly. Instead, try again through CAS to see if the lock can be obtained. If the lock is obtained, it will return directly, indicating that the lock is obtained successfully. Otherwise, you need to queue the thread. Note that the template design pattern is used here.

Interpretation of acquire() method

final int acquire(Node node, int arg, boolean shared,
                      boolean interruptible, boolean timed, long time) {
        Thread current = Thread.currentThread();
        byte spins = 0, postSpins = 0;   // retries upon unpark of first thread
        boolean interrupted = false, first = false;
        Node pred = null;                // predecessor of node when enqueued

        /*
         * Repeatedly:
         *  Check if node now first
         *    if so, ensure head stable, else ensure valid predecessor
         *  if node is first or not yet enqueued, try acquiring
         *  else if node not yet created, create it
         *  else if not yet enqueued, try once to enqueue
         *  else if woken from park, retry (up to postSpins times)
         *  else if WAITING status not set, set and retry
         *  else park and clear WAITING status, and check cancellation
         */

        for (;;) {
            if (!first && (pred = (node == null) ? null : node.prev) != null &&
                !(first = (head == pred))) {
                if (pred.status < 0) {
                    cleanQueue();           // predecessor cancelled
                    continue;
                } else if (pred.prev == null) {
                    Thread.onSpinWait();    // ensure serialization
                    continue;
                }
            }
            if (first || pred == null) {
                boolean acquired;
                try {
                    if (shared)
                        acquired = (tryAcquireShared(arg) >= 0);
                    else
                        acquired = tryAcquire(arg);
                } catch (Throwable ex) {
                    cancelAcquire(node, interrupted, false);
                    throw ex;
                }
                if (acquired) {
                    if (first) {
                        node.prev = null;
                        head = node;
                        pred.next = null;
                        node.waiter = null;
                        if (shared)
                            signalNextIfShared(node);
                        if (interrupted)
                            current.interrupt();
                    }
                    return 1;
                }
            }
            if (node == null) {                 // allocate; retry before enqueue
                if (shared)
                    node = new SharedNode();
                else
                    node = new ExclusiveNode();
            } else if (pred == null) {          // try to enqueue
                node.waiter = current;
                Node t = tail;
                node.setPrevRelaxed(t);         // avoid unnecessary fence
                if (t == null)
                    tryInitializeHead();
                else if (!casTail(t, node))
                    node.setPrevRelaxed(null);  // back out
                else
                    t.next = node;
            } else if (first && spins != 0) {
                --spins;                        // reduce unfairness on rewaits
                Thread.onSpinWait();
            } else if (node.status == 0) {
                node.status = WAITING;          // enable signal and recheck
            } else {
                long nanos;
                spins = postSpins = (byte)((postSpins << 1) | 1);
                if (!timed)
                    LockSupport.park(this);
                else if ((nanos = time - System.nanoTime()) > 0L)
                    LockSupport.parkNanos(this, nanos);
                else
                    break;
                node.clearStatus();
                if ((interrupted |= Thread.interrupted()) && interruptible)
                    break;
            }
        }
        return cancelAcquire(node, interrupted, interruptible);
    }

Mainly analyze the following two codes

 

The 4 codes are the same as the interpretation method of the tryAcquire(1) method above, and will not be repeated.

The 5 codes obtain the lock through CAS, proving that the previous thread has released the lock, then set the previous node to null for GC recycling, set the current node as the head node, and set the waiting thread on the current node to null.

The code at 6 creates a new node. The status value of the node is status=0.

The code at 7 sets the waiting thread on the node node as the current thread, and sets the new node as the tail node. If the status on the node is 0, it will be changed to 1.

The code at 8 sets the current thread to a blocked state.

The general process is shown in the figure below

Release lock operation

Interpretation of tryRelease(1) method

9 codes, as in the above locking process, the state value of each lock added is increased by 1, and the corresponding state value of each lock released is reduced by 1

10 codes to verify whether the thread releasing the lock is exclusive. If it is not, throw an exception. In plain language, it is who added the lock and who needs to release it.

11 codes. If the state value is reduced to 0, the exclusive thread will be left empty and other threads will compete again.

Interpretation of signalNext(Node h) method

This operation is relatively simple, that is, wake up the thread on the next node of the current node. But throwing a problem to wake up the thread on the next node sounds like a fair lock, but it's not. At this time, if a new thread comes in instead of the thread in the queue, the new thread will compete with the thread about to wake up. Let's take a look at the lock grabbing code of fair lock.

Interpretation of hasQueuedThreads() method

This method is interesting. It starts from the end of the linked list. If the status value is greater than or equal to 0, it returns true, but as long as the node status queued in the linked list is either equal to 0 or 1. This verifies whether the thread competing for resources is the thread about to be awakened to realize the principle of fair lock.

reference resources:

Java concurrency - theoretical basis | Java full stack knowledge system

JAVA high concurrency core programming (Volume 2): multithreading, locking, JMM, JUC, high concurrency design - edited by Nien

Topics: Concurrent Programming source code analysis JUC aqs