AQS Source Code Analysis

Keywords: Programming

Wechat Public Number: Madad's technical wheel

JUC implements AQS tree:

brief introduction

AQS (AbstractQueued Synchronizer) is an abstract queue synchronizer. AQS defines a synchronizer framework for multithreaded access to shared resources. Many classes in the juc package are implemented based on this class, such as ReentrantLock, CountDownLatch, etc.
There are two linked lists throughout AQS. A list is the entire Sync Node list, a horizontal list. Another list is Condition's Wait Node list, which belongs to a vertical list of node nodes relative to Sync node. When the vertical list is notified by single, it enters the corresponding Sync Node for queuing.

volatile keyword

  • Atomicity (not necessarily guaranteeing its atomicity, such as i++)

  • visibility

  • Sequentially

Introduction of Reservation Method

The following five protected methods are used for client self-implementation and business behavior control; many implementations of Lock and Futrue under juc package are based on this extension

  • tryAcquire(int): Exclusive way, try to get resources, success returns true, failure returns false

  • tryRelease(int): In exclusive mode, try to release resources, return true for success, false for failure

  • tryAcquireShared(int): Sharing, trying to get resources. Negative numbers denote failure; 0 denotes success, but no remaining resources are available; positive numbers denote success, with remaining resources

  • tryReleaseShared(int): Sharing mode, trying to release resources, if it allows waking-up of subsequent waiting nodes to return true, otherwise it returns false.

  • isHeldExclusively(): Is this thread monopolizing resources? Only condition is needed to implement it.

Exclusive Lock and Shared Lock Cases

  • In the case of ReentrantLock exclusive lock, the state is initialized to 0, indicating the unlocked state. When thread Alock, tryAcquire is invoked to monopolize the lock and case is set to state 1. At this point, other threads fail when they come to try Acquire until thread A calls unlock to release the lock and set state to 0. Of course, thread A itself can retrieve the lock (state accumulation) repeatedly before releasing the lock, which is the reentrant concept. But pay attention to how many times you get it and how many times you release it to ensure that the state returns to zero.

  • Take CountDownLatch shared lock as an example. Tasks are divided into N sub-threads to execute, and state is also N (state must be consistent with the number of threads). The N sub-threads are executed in parallel. When each sub-thread executes countDown method, state is subtracted by 1 and cas is set to state. After all sub-threads execute (state=0), unpark() wakes up and waits for the main thread to perform subsequent operations.

Node design

 static final class Node {
        //Shared lock mode, where multiple threads can execute simultaneously, such as Semaphore/CountDownLatch
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        //Exclusive lock mode, where only one thread can execute, such as ReentrantLock
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;

        //WatStatus status
        //Represents that the current node has been cancelled. When timeout or interruption occurs (in the case of response interruption), a change to this state is triggered, and the node entering that state will not change again.
        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        //Represents the need for the current Node node to wake up the latter Node node. When the Node node enqueue, the state of the previous node is set. This chain wake-up, complete such a handover rod
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL = -1;
        //When other threads call the signal() method of Condition, the node in the CONDITION state will be transferred from the waiting queue to the synchronization queue, waiting for the synchronization lock to be acquired.
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        //In shared mode, the successor node will not only wake up its successor node, but also may wake up its successor node (propagation).
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;

        //State, new node defaults to 0
        volatile int waitStatus;
        //Linked list successor node
        volatile Node prev;
        //Link list successor node
        volatile Node next;
        //Queued threads of this node, initialized as null
        volatile Thread thread;
        //A flag bit is used to indicate whether a shared lock or an exclusive lock is used. It is also a reference node for its corresponding condition queue
        Node nextWaiter;
}

Node design should pay attention to the following points:

  • Node implements CLH locks; Craig, Landin, and Hagersten (CLH) locks. CLH lock is a spin lock. Ensuring Starvation-free and fairness in providing first-come, first-served services

  • It is an implementation of FIFO linked list, and double-check is often used for queue control.

  • The negative value of waitStatus represents that the node is in a waiting state, while the positive value represents that the node has been cancelled.

enqueue analysis (double-check)

    /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    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) {
            //Point the prev of the current node to the tail node
            node.prev = pred;
            if (compareAndSetTail(pred, node)) { //Location 1
                //The next node of tail node points to the current node
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { //Must initialize//location 3
                if (compareAndSetHead(new Node())) //Location 4
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) { //Location 2
                    t.next = node;
                    return t;
                }
            }
        }
    }

Four locations of the above code tags:

  1. The default enqueue operation of node is connected to the tail node. After the prev node is specified, a case operation is performed, which takes the currently joined node as the tail. Because there will be concurrent operations, the original tail node will change, and cas fails at location 1. So go to step 2 check

  2. Location 2: Get the current tail node one time at a time, try to do cas operation, and regard the current node as tail. There is concurrency/competition which leads to processing failure. Continue to repeat this action until the cas succeeds.

  3. An exception handled in location 1 and 2 is that the tail node is empty. It is possible that the current node is the first to enqueue. At location 4, a new queue needs to be created. Case sets up an empty Head Node node and regards itself as a tail node. Another exception might be that node nodes are awakened by all

  4. Similarly, considering the concurrency factor, location 3 may be processed by other threads that have created the Head Node node, which can then be processed back on location 2 and added to the tail node.

Team dequeue

Departure is divided into two actions:

1. actions

    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor(); //Location 1
                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);
        }
    }

    /**
     * Sets head of queue to be node, thus dequeuing. Called only by
     * acquire methods.  Also nulls out unused fields for sake of GC
     * and to suppress unnecessary signals and traversals.
     *
     * @param node the node
     */
    private void setHead(Node node) { //Location 2
        head = node;
        node.thread = null;
        node.prev = null;
    }

Two locations of the above code tags:

  • At position 1, the prev front node of the current node is retrieved each time to determine whether it is equal to the head node. Here, the head node is abstract and a little difficult to understand. It can be understood as a "virtual" or "puppet" node, which simply represents the last node out of the library. Because it's a FIFO queue, if the last node of the current node has been out of the library, it's your turn.

  • At position 2, the wheel goes out of the library by itself, takes the current node as the head, and carries on the gc processing at the same time, manually disconnects some associations between node and linked list.

2. action 2

    /**
     * Wakes up node's successor, if one exists.
     *
     * @param node the node
     */
    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) { //Location 1
            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);
    }

This method is called when the lock is released; at position 1, the loop skips the canceled node, and finally unpark wakes up the thread of the corresponding node

Source code analysis

** Exclusive Locks**

  • require

    /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor(); //Location 1
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) && //Location 2
                    parkAndCheckInterrupt()) //Location 3
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node); //Location 4
        }
    }

    /**
     * Checks and updates status for a node that failed to acquire.
     * Returns true if thread should block. This is the main signal
     * control in all acquire loops.  Requires that pred == node.prev.
     *
     * @param pred node's predecessor holding status
     * @param node the node
     * @return {@code true} if thread should block
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL) //If it is SIGNAL, you need park
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) { //Handling cancelled node nodes
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else { //cas sets the waitStatus of the previous node
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

    /**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
  1. tryAcquire custom extensions, which generally control value values, are similar to P/V primitive control
    P: Apply for an idle resource (reduce the semaphore by 1) and exit if it succeeds; if it fails, the process is blocked.
    V: Release an occupied resource (add a semaphore to 1) and choose a wake-up if a blocked process is found.

  2. Add Waiter, add Node to the list of links, there are explanations for entry.

  3. acquireQueued analysis:

  • Location 1 is to compare whether my last node has been out of the queue, and if it has already been out of the queue, I think it's my turn to go out of the queue and return to the interrupted flag.

  • At position 2, shouldPark AfterFailed Acquire is executed, which is to set the waitStatus status of the previous node of the current node to SINGLE, so that it can wake itself up for processing when it is out of the queue.

  • After the last node is set to SINGLE, the current thread can park and go to the blocking state until it is awakened. (There are two wake-up conditions: the wake-up of the previous node and the Thread.interupte event)

  • Location 4 is a cancel.

  • acquireInterruptibly interrupt

   public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted()) //Location 1
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

    /**
     * Acquires in exclusive interruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException(); //Location 2
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

The difference between acquire and acquire Interruptibly is that Location 1 determines whether the thread is interrupted or not, and if it is interrupted, the exception thrown is handled by the caller; Location 2 throws the InterruptedException interrupt exception directly.

  • tryAcquireNanos timeout

    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

    /**
     * Acquires in exclusive timed mode.
     *
     * @param arg the acquire argument
     * @param nanosTimeout max wait time
     * @return {@code true} if acquired
     */
    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        //Calculating deadlines
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                //Calculating timeout time
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L) //Location 1
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold) //Location 2
                    LockSupport.parkNanos(this, nanosTimeout); //Location 3
                if (Thread.interrupted()) //Location 4
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

tryAcquireNanos supports setting timeouts and returning false without acquiring a lock at a specified time

  • Location 1, if timeout returns directly to false

  • Location 2, if the calculated timeout time is greater than the spinlock timeout threshold, then position 3 parkNanos blocking timeout time

  • Location 4, thread interrupt throws interrupt exception directly

** Shared Locks**

  • acquireShared

   public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

    /**
     * Acquires in shared uninterruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r); //Location 1
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    /**
     * Sets head of queue, and checks if successor may be waiting
     * in shared mode, if so propagating if either propagate > 0 or
     * PROPAGATE status was set.
     *
     * @param node the node
     * @param propagate the return value from a tryAcquireShared
     */
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        /*
         * Try to signal next queued node if:
         *   Propagation was indicated by caller,
         *     or was recorded (as h.waitStatus either before
         *     or after setHead) by a previous operation
         *     (note: this uses sign-check of waitStatus because
         *      PROPAGATE status may transition to SIGNAL.)
         * and
         *   The next node is waiting in shared mode,
         *     or we don't know, because it appears null
         *
         * The conservatism in both of these checks may cause
         * unnecessary wake-ups, but only when there are multiple
         * racing acquires/releases, so most need signals now or soon
         * anyway.
         */
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared(); //Location 2
        }
    }
  • At location 1, set the queue head and check whether successors can wait in shared mode, and if propagation is in progress, whether propagation is set to propagation > 0 or PROPAGATE status.

  • Location 2, release locks, wake up the next Node when you leave the queue

ConditionObject

Use scenarios: producers and consumers
Overall, the implementation of this ConditionObject actually maintains two queues:

  • Condition queue: A queue representing a waiting queue whose waitStatus=Node.Condition is manipulated by the first and last Waiter attributes

  • Sync queue: A queue that can compete for locks, which is consistent with AQS, waitStatus=0

  • await() method: create a Node for the current thread to join the Condition queue, and then keep looping to see if it is not in the Sync queue. If the current node is in the Sync queue, it can compete for the lock and resume running.

  • signal() method: Set the nextWaiter of a node to null, and then transfer it from the Condition queue to the Sync queue.

Posted by erikhillis on Fri, 27 Sep 2019 02:06:38 -0700