[Big Data Java Foundation - Java Concurrency 18] AQS of J.U.C: Acquisition and Release of Synchronization State

As mentioned earlier, AQS is the basis for building Java synchronization components, and we expect it to be the basis for most synchronization needs. AQS design mode uses template method mode, subclasses implement its abstract method to manage synchronization state through inheritance. For subclasses, it does not have much work to do. AQS provides a large number of template methods to achieve synchronization, which are mainly divided into three categories: exclusive acquisition and release of synchronization state, shared acquisition and release of synchronization state, Query the wait threads in the synchronization queue. Custom subclasses use template methods provided by AQS to implement their own synchronization semantics.

exclusive

Exclusive, with only one thread holding synchronization at a time.

Exclusive Synchronization State Acquisition

The acquire(int arg) method provides a template method for AQS, which is an exclusive method to get synchronization status, but is insensitive to interrupts, which means that threads are not removed from the synchronization queue when subsequent interrupt operations occur because they fail to get synchronization status. The code is as follows:
 

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

The methods are defined as follows:

  1. tryAcquire: To try to acquire a lock, set the lock state and return true if successful, otherwise return false. This method implements the custom synchronization component itself, which must ensure thread-safe access to the synchronization state.
  2. addWaiter: If tryAcquire returns FALSE (Failed to get synchronization status), call this method to join the current thread to the end of the CLH synchronization queue.
  3. acquireQueued: The current thread will block waiting (spin) based on fairness until a lock is acquired; And returns whether the current thread has been interrupted while waiting.
  4. selfInterrupt: Generates an interrupt.

The acquireQueued method is a spin process, that is, when the current thread (Node) enters the synchronization queue, it enters a spin process, and each node observes it introspectively. When the condition is met and the synchronization state is obtained, it can exit from the spin process, otherwise it will continue to execute. The following:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            //Interrupt flag
            boolean interrupted = false;
            /*
             * The spin process is actually a dead cycle
             */
            for (;;) {
                //The precursor node of the current thread
                final Node p = node.predecessor();
                //The precursor node of the current thread is the header node and the synchronization status is successful
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //Get failed, thread waiting--described later
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

As you can see from the code above, the current thread will always try to get synchronization status, provided that it can only try to get synchronization status if its precursor node is a header node, for the following reasons:

  1. Maintain FIFO synchronization queue principle.
  2. When a header node releases its synchronization state, its successor nodes are awakened, and they need to check whether they are headers after they are awakened.

The acquire(int arg) method flowchart is as follows:

Exclusive Get Response Interrupt

AQS provides the acquire(int arg) method to obtain synchronization status exclusively, but it does not respond to interrupts and, after interrupting a thread, it remains in the CLH synchronization queue waiting to get synchronization status. In response to interrupts, AQS provides the acquireInterruptibly(int arg) method, which, while waiting to get synchronization status, throws an exception InterruptedException in response to interrupts if the current thread is interrupted.

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

First check if the thread has been interrupted and if it throws an InterruptedException, otherwise execute the tryAcquire(int arg) method to get the synchronization status, if it succeeds, return directly, otherwise execute the doAcquireInterruptibly(int arg). doAcquireInterruptibly(int arg) is defined as follows:

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();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

The doAcquireInterruptibly(int arg) method differs from the acquire(int arg) method in only two ways. 1. Method declarations throw InterruptedException exceptions. 2. Instead of using the interrupted flag at the break method, InterruptedException exceptions are thrown directly.

Exclusive Timeout Acquisition

In addition to providing the above two methods, AQS also provides an enhanced version: tryAcquireNanos(int arg,long nanos). This method is a further enhancement of the acquireInterruptibly method, which has timeout control in addition to responding to interrupts. That is

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

The tryAcquireNanos(int arg, long nanosTimeout) method timeout acquisition is ultimately implemented in doAcquireNanos(int arg, long nanosTimeout), as follows:

private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        //nanosTimeout <= 0
        if (nanosTimeout <= 0L)
            return false;
        //timeout
        final long deadline = System.nanoTime() + nanosTimeout;
        //Add Node Node
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            //spin
            for (;;) {
                final Node p = node.predecessor();
                //Get Synchronization Status Successful
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                /*
                 * Get Failure, Make Timeout, Break Judgment
                 */
                //Recalculate the time needed to sleep
                nanosTimeout = deadline - System.nanoTime();
                //Timed out, returning false
                if (nanosTimeout <= 0L)
                    return false;
                //Wait nanos Timeout nanoseconds if no timeout occurs
                //Note: This thread returns directly from LockSupport.parkNanos.
                //LockSupport provides a blocking and waking tool class for JUC, which is described in more detail later
                if (shouldParkAfterFailedAcquire(p, node) &&
                        nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                //Whether the thread has been interrupted
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

For timeout control, the program first records the wake-up time deadline, deadline = System.nanoTime() + nanosTimeout (time interval). If getting synchronization status fails, you need to calculate the time interval nanosTimeout (= deadline - System.nanoTime()) needs to sleep, if nanosTimeout <= 0 indicates that it has timed out, return false, if it is larger than spinForTimeoutThreshold (1000L), you need to sleep nanosTimeout, if nanosTimeout <= spinForTimeoutThreshold, you do not need to sleep. Directly into the fast spin process. The reason is that spinForTimeoutThreshold is so small that a very short wait time can't be very precise. If you wait for the timeout again, it will make the nanosTimeout's timeout less less overall less precise, so in very short timeout scenarios, AQS spins unconditionally fast.

The whole process is as follows:

Exclusive Synchronization State Release

When a thread acquires the synchronization state, it needs to release the synchronization state after executing the appropriate logic. AQS provides release(int arg) methods to release synchronization states:

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

This method also calls the custom synchronizer's custom tryRelease(int arg) method to release the synchronization state first. After successful release, the unparkSuccessor(Node node) method is called to wake up the subsequent nodes (described later on how to wake up LZ).

Here's a little summary:

A FIFO synchronization queue is maintained in AQS. When a thread fails to get synchronization status, it joins the end of the CLH synchronization queue and remains spinning. Threads in the CLH synchronization queue spin to determine if their precursor node is the first node, and if the first node keeps trying to get synchronization status, they exit the CLH synchronization queue if they succeed. When a thread finishes executing its logic, it releases the synchronization state and wakes up its successor nodes.

Shared

The main difference between shared and exclusive is that only one thread can get synchronization state at the same time, while shared can get synchronization state by multiple threads at the same time. For example, a read operation can have multiple threads simultaneously, while a write operation can only have one thread writing at a time, and all other operations will be blocked.

Shared synchronization status acquisition

AQS provides acquireShared(int arg) methods to share to get synchronization status:

  public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
			//Get failed, spin to get synchronization status
            doAcquireShared(arg);
    }

From the above program, it can be seen that the method first calls the tryAcquireShared(int arg) method to try to get the synchronization state, and if the acquisition fails, calls the doAcquireShared(int arg) spin method to get the synchronization state. The flag for the shared acquisition of the synchronization state is to return a value >= 0 indicating success. Auto-fetch synchronization status is as follows:

 private void doAcquireShared(int arg) {
        /Shared Node
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //Precursor Node
                final Node p = node.predecessor();
                //Get synchronization status if its precursor node
                if (p == head) {
                    //Try to get synchronization
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

The tryAcquireShared(int arg) method attempts to get the synchronization status, returning an int value, which when >= 0 indicates that the synchronization status can be obtained, at which point it can exit from the spinning process.

The acquireShared(int arg) method does not respond to interrupts. Similar to exclusive, AQS also provides methods to respond to interrupts and timeouts: acquireSharedInterruptibly(int arg), tryAcquireSharedNanos(int arg,long nanos), which are not explained here.

Shared Synchronization State Release

After getting the synchronization state, you need to call the release(int arg) method to release the synchronization state, as follows:

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

Since there may be multiple threads releasing synchronization state resources at the same time, you need to ensure that the synchronization state is released safely and successfully, typically through CAS and loops.

 

Tags: Java C Big Data

Posted on Tue, 02 Nov 2021 13:51:45 -0400 by zoran