JAVA Concurrent Programming -- AQS concept and source code reading of AbstractQueuedSynchronizer

1. What is AQS

2. What can I do

3. Why is AQS the most important cornerstone of JUC content

4.AQS internal architecture

5. Interpret AQS from our ReentrantLock

6. Summary

1. What is AQS
AQS -- full name AbstractQueuedSynchronizer, abstract queue synchronizer.
We can see the explanation in the source code:

In other words, it is a heavyweight basic framework used to build locks or other synchronizer components and the cornerstone of the whole JUC. It completes the queuing of resource acquisition threads through the built-in FIFO queue, and represents the state of holding locks through an int class variable.

In short
1) This is a cornerstone framework( Template method design pattern)
2) Is to queue up the threads that get resources and let them get resources in order.

2. What can I do
Suppose that when we use ReentrantLock, there must be thread blocking. If there is thread blocking, we must queue, and we must queue.
The thread that fails to preempt resources continues to wait, but the waiting thread still retains the possibility of obtaining locks, and the process of obtaining locks continues.
This is AQS. We build locks and queues and let threads obtain resources orderly.

3. Why is AQS the most important cornerstone of JUC content

Let's take a look at the specific classes that implement AQS:

There are ReentrantLock, CountDownLatch, ReentrantReadWriteLock, Semaphore, etc.

At this point, we need to clarify a relationship:
Lock: it is aimed at lock users. It defines the application layer api for programmers and lock interaction, hides the implementation details, and can be called by developers.

Synchronizer: Lock oriented implementers, such as concurrent God Doug Lee, put forward a set of specifications, simplified the implementation of locks, shielded the management of synchronization status, blocked thread queuing and notification, wake-up mechanism, etc.

4.AQS internal architecture

Let's click on the source code:

Let's search the following variable

/**
* The synchronization state.
*/
private volatile int state;

AQS uses the above volatile int type variable to represent the synchronization state of the occupied lock

Search the following variable again:

static final class Node {

//There are still some variables that have not been put out. Focus on explaining these variables

volatile Node prev;

volatile Node next;

volatile Thread thread;
//....

}

AQS uses the FIFO pairing of this linked list to complete the queuing of resource acquisition, encapsulates each thread to be preempted into a Node node to realize lock allocation, and modifies the state value through CAS.

In other words, we implement the basic structure of AQS through queue (managing threads that need to queue) + state variable (managing public resource classes).

Let's take another look at the internal Node:

volatile int waitStatus;

Node blocks or wakes up the threads in the queue through the member variable of waitState. The status of waitStatus is shown in the figure, but you can also see the source code comments

Finally, let's use a diagram to show the basic structure of AQS

5. Interpret AQS from our ReentrantLock

Let's take ReentrantLock as a breakthrough point to read the source code of AQS.

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
            TimeUnit.MINUTES.sleep(60);   
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

When we click lock, we can see that the bottom layer of lock is a lock method that calls a sync variable

    public void lock() {
        sync.lock();
    }

sync:

abstract static class Sync extends AbstractQueuedSynchronizer {
//......
}

It can be seen that:
The implementation class of Lock interface basically completes thread access control by aggregating a subclass of queue synchronizer.

If we continue to click inside the lock, we will find that there is a fair lock and an unfair lock:

Then we continue to click inside to check the acquire method of preemptive lock, and we will find the difference in the figure

When an unfair lock preempts resources, it is not necessary to determine whether there are queuing threads in front of the queue.

When the fair lock preempts resources, it is necessary to determine whether there is a queued thread in front of the degree column.

In fact, we only need to understand the source code of non-fair lock, in fact, we also understand the source code of fair lock.

Let's first sort out the overall process of obtaining locks:

Let's start with the lock method of NonfairSync:

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            //The current thread is trying to change the state of the lock resource from 0 to 1
            //If successful
            //Set the thread occupied by resources as this thread
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
            //If it fails
            //Take the acquire method
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

Continue to view the acquire(1) method

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

tryAcquire:

        protected final boolean tryAcquire(int acquires) {
            //Click this method to go to the following method
            return nonfairTryAcquire(acquires);
        }


        final boolean nonfairTryAcquire(int acquires) {
            //Get the current thread first
            final Thread current = Thread.currentThread();
            int c = getState();
            //If the status of the current lock is 0 (unoccupied status)
            if (c == 0) {
                //Try to use cas to occupy
                if (compareAndSetState(0, acquires)) {
                    //Set the thread occupying this resource as this thread
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //If the status of the current lock is not 0, but the thread occupying the lock is this thread
            else if (current == getExclusiveOwnerThread()) {
                //It is equivalent to re entering the lock, and then the status flag bit + 1
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

addWaiter:

    private Node addWaiter(Node mode) {
       //Set the current thread as a Node
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
       //If the tail node is not empty
        Node pred = tail;
        if (pred != null) {
            //Set the previous node of this thread as the tail node
            node.prev = pred;
            //Exchanging tail nodes using cas
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //Initializes if the tail node is empty
        enq(node);
        return node;
    }

    private Node enq(final Node node) {
        //Dead cycle
        for (;;) {
            Node t = tail;
            //If the tail node is empty, a dummy node is created
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
             //If the tail node is not empty, the tail node is exchanged and returned, jumping out of the loop
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

acquireQueued:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                //If the previous node of the node is the head node (there are no other nodes in front of the queue), and the lock scrambling is successful
                if (p == head && tryAcquire(arg)) { 
                    //Set the current node as the head node
                    setHead(node);
                    //The next node of the original head node is empty to help GC
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //If the previous node of a node is not a cast node
                //Change the running state of the current node
                if (shouldParkAfterFailedAcquire(p, node) &&
                    //Pause the thread
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

selfInterrupt:

    Returns the interrupt status of the thread
    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

Let's look at the unlock method:

    public final boolean release(int arg) {
         //An attempt is made to release the lock. If the status of the lock is 0, the release is successful
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                //Wake up header node
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

6. Summary

Today, we learned about the concept and source code of AQS. As the cornerstone framework of the whole JUC, AQS is very important.

Keywords: Java Concurrent Programming aqs

Added by techrat on Wed, 05 Jan 2022 06:43:59 +0200