Principle analysis of AbstractQueuedSynchronizer

Posted by bobthebuilder on Tue, 16 Jun 2020 07:01:11 +0200

Source of this article Principle analysis of AbstractQueuedSynchronizer
Reprint please specify

Abstract queuedsynchronizer, AQS for short, is a common dependency framework for most of Java, such as Lock, Semaphore, CountDownLatch, etc., which implements blocking locks that rely on FIFO waiting queues. Reading its code principle is helpful for us to understand the Java Lock derived class principle and help us develop custom Lock.

Main principles


As shown in the above figure, the elements in the queue are all execution threads. The head of the queue is the execution thread that obtains the exclusive lock. Other threads are sleeping in the queue. The thread that wants to acquire the lock will join the queue from the tail. When the head releases the lock, it wakes up the next thread to acquire the lock, and points the head to the next thread after obtaining the lock successfully. Here is a brief introduction to the basic principles. Let's enter the code explanation together.

Node internal class properties

    static final class Node {
        /** Mark current node shared lock mark */
        static final Node SHARED = new Node();
        /** Mark current node exclusive lock mark */
        static final Node EXCLUSIVE = null;

        /** Interrupt or timeout exit lock competition   */
        static final int CANCELLED =  1;
        /**  When the lock is about to be released, you need to wake up the next acquisition thread and point the head node to the next node */
        static final int SIGNAL    = -1;
        /** Synchronization queue condition     */
        static final int CONDITION = -2;
        //Shared lock
        static final int PROPAGATE = -3;

        //Thread waiting states correspond to the above four states respectively
        volatile int waitStatus;
        //Front node reference
        volatile Node prev;
        //Post node reference
        volatile Node next;

        volatile Thread thread;
        //Next node to wake up
        Node nextWaiter;

        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        Node() {}

        /** Constructor used by addWaiter. */
        Node(Node nextWaiter) {
            this.nextWaiter = nextWaiter;
            THREAD.set(this, Thread.currentThread());
        }

        /** Constructor used by addConditionWaiter. */
        Node(int waitStatus) {
            WAITSTATUS.set(this, waitStatus);
            THREAD.set(this, Thread.currentThread());
        }

AbstractQueuedSynchronizer internal properties


    /**
     * Queue header
     */
    private transient volatile Node head;

    /**
     * Queue tail
     */
    private transient volatile Node tail;

    /**
     * Synchronization status. Obtain the lock according to this value. The default thread starts from 0
     */
    private volatile int state;

    protected final int getState() {
        return state;
    }

    protected final void setState(int newState) {
        state = newState;
    }

    protected final boolean compareAndSetState(int expect, int update) {
        return STATE.compareAndSet(this, expect, update);
    }

When entering ReentrantLock and acquiring the lock, call the AQS method

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

Sync is an internal class of ReentrantLock, which inherits from AbstractQueuedSynchronizer and is used to implement fair locks and unfair locks.

acquire

    public final void acquire(int arg) {
         //Try to get the lock
        if (!tryAcquire(arg) && 
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  //Failed to acquire lock, initialize queue first and then queue
            selfInterrupt(); //Set current thread interrupt
    }

The main process obtains the lock first. If it fails, it enters the queue.

acquireQueued

    final boolean acquireQueued(final Node node, int arg) {
        boolean interrupted = false;
        try {
            for (;;) { //spin 
                final Node p = node.predecessor(); //Preposition node
                if (p == head && tryAcquire(arg)) {//The front node is head, and the next attempt to acquire a lock is to acquire a lock
                    setHead(node);  //Get success, point the head to node
                    p.next = null; // help GC  
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node))  //Judge whether the predecessor node waitStatus is SIGNAL, if not, the thread will not be suspended
                    interrupted |= parkAndCheckInterrupt(); //Suspend the thread. When the thread is awakened, it will return to thread interrupt state and clear the interrupt
            }
        } catch (Throwable t) {
            cancelAcquire(node); //Wake up the thread and remove the blocking queue
            if (interrupted)
                selfInterrupt();
            throw t;
        }
    }

In the acquirequeueued method, either the lock is acquired successfully to jump out of the loop, or an exception occurs to enter the exception handling logic.

shouldParkAfterFailedAcquire

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL) //Thread ready to wake up
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) { //If it is greater than 0, the thread will exit the lock competition 
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node; //Delete queue node with WS = canceled
        } else { // Either 0 or promote is changed to 
            /*
             * 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.
             */
            pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
        }
        return false;
    }

This method has two functions: comparing waitStatus or setting to move the node forward.
When p=head is equal, it may be head or empty, and the queue has not been initialized successfully, then the lock is acquired once. The other is that the node is the head post node, or the node tries to acquire the lock after re entering. The lock is acquired successfully, and the head node is reset.

cancelAcquire

    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null) 
            return;

        node.thread = null;

        Node pred = node.prev;
        while (pred.waitStatus > 0)  //Skip status greater than 0 thread
            node.prev = pred = pred.prev;

        Node predNext = pred.next;

        //Set the current node to cancel led state to exit lock competition
        node.waitStatus = Node.CANCELLED;

        //If the node is tail, directly set the front node to tail,
        if (node == tail && compareAndSetTail(node, pred)) {
            pred.compareAndSetNext(predNext, null); //take tail.next = null 
        } else {
            int ws;
             // To judge whether the front node cannot be head, and then judge whether the state of the node's successor node is legal when the state is SIGNAL
            // So the conditions are all tenable. Associating the two nodes before and after is equivalent to deleting itself directly.
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    pred.compareAndSetNext(predNext, next);//Anterior posterior integration
            } else {
                unparkSuccessor(node); //Wake up post node
            }

            node.next = node; // help GC
        }
    }

The main task of this method is to find the front node with legal status, set the thread status cancolled, exit the thread lock competition, find the back thread, associate the front and back instructions, and delete yourself from the queue. But it is possible that the front node head needs to wake up the rear node, delete the CANCELLED node and acquire the lock

unparkSuccessor

See how to wake up the thread

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0) //Because to wake up the next thread, the SIGNAL state should be cleared
            node.compareAndSetWaitStatus(ws, 0);

        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node p = tail; p != node && p != null; p = p.prev) //Traverse from back to front to find the closest node
                if (p.waitStatus <= 0)
                    s = p;
        }
        if (s != null)
            LockSupport.unpark(s.thread); //Wake up thread
    }

addWaiter

    private Node addWaiter(Node mode) {
        Node node = new Node(mode); 

        for (;;) {
            Node oldTail = tail;
            if (oldTail != null) {//Tail already exists, add thread to tail
                node.setPrevRelaxed(oldTail);
                if (compareAndSetTail(oldTail, node)) {
                    oldTail.next = node;
                    return node;
                }
            } else {
                initializeSyncQueue(); //Initialize queue head and tail properties
            }
        }
    }

According to the given mode to create a queuing node, mode is divided into two modes Node.EXCLUSIVE Exclusive lock and Node.SHARED Shared lock.

Combined with the analysis of the above three methods, we have some simple understanding of the failure of obtaining lock. The thread uses tryAcquire to acquire the lock. If the acquisition fails, it enters the queuing method. At this time, it first determines that the queue tail exists, and there is already a direct tail insertion method. Otherwise, it initializes the queue tail and head first. It is known that the queue initialization is not initiated by acquiring lock first, but by the thread that fails to compete. This is for performance. Acquire will continue to acquire locks according to the situation that the front node is head, and set an uninterrupted lock competition trend. Then suspend the current thread. When the thread is awakened, the first thing to do is to get the lock successfully and jump out of the acquire loop. Returns the thread interrupt state. Only when the thread interrupt state exists, the thread interrupt is called again.
In some cases, the thread queue picture at the beginning of this article is not the same as the picture display. At the beginning of the queue initialization, the head Node is not the Node to acquire the lock. The head Node creates the Node object randomly, which is the same as the tail. When a thread from tail.next Association, enter the queue, and also introduce the relationship with the head. As long as the Node closest to the head obtains the lock, it will point the head to itself before going to the direction of the opening queue picture.

Analyze tryAcquire

tryAcquire is the only implementation of acquiring lock, which is mainly implemented by subclass. The main reason is that AQS supports exclusive lock and shared lock, and whether it supports reentry. It is more suitable for subclass to implement the lock acquisition logic. Enter ReentranLock to learn how to implement tryAcquire for fair lock and unfair lock. Analyze the two situations together.

Unfair lock

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

    abstract static class Sync extends AbstractQueuedSynchronizer {
        @ReservedStackAccess
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {//state competes for lock for the first time
                if (compareAndSetState(0, acquires)) { //If the exchange comparison state is successful, the lock is obtained successfully
                    setExclusiveOwnerThread(current);//Set property variable
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) { //Lock reentry, only for state + = acquire
                int nextc = c + acquires;
                if (nextc < 0) // int out of bounds
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
}

It is very simple. CAS (exchange comparison) is used to set the successful thread first. Even if it is successful, it can be used to determine whether the thread acquiring lock is the thread occupying lock and support re-entry lock.

Fair lock

    static final class FairSync extends Sync {
        @ReservedStackAccess
        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;
        }
    }

The main difference is that there is an additional judgment of hasQueuedPredecessors, which is under method analysis

    public final boolean hasQueuedPredecessors() {
        Node h, s;
        if ((h = head) != null) {//Queue already initialized
            //If it is greater than 0, it is canceled. s will be removed from the queue. The next node is not allowed. Get the next node
            if ((s = h.next) == null || s.waitStatus > 0) { 
                s = null; // traverse in case of concurrent cancellation
                for (Node p = tail; p != h && p != null; p = p.prev) { //Traversal from tail chain to front
                    if (p.waitStatus <= 0) //waitStatus status OK
                        s = p;
                }
            }
            if (s != null && s.thread != Thread.currentThread()) //The thread to acquire the lock as long as it is not the next thread to acquire the lock
                return true;
        }
        return false;
    }

First, judge whether the head node is initialized. If not, return false directly. There is already a successor node that directly obtains the head node. Only when the node thread is judged to be unequal to the current thread, it returns true. Why is that? Think about when the lock starts to be released, wake up the next node to acquire the lock. At this time, a thread will also acquire the lock. It is possible to seize the queued node in the queue to acquire the lock, which is equivalent to queue jumping, which is "unfair".
The main difference between public and unfair is to judge whether there is a queuing thread in the queue before acquiring the lock. If there is any, the principle of directly acquiring the lock fails, entering the queue, and forcibly ensuring that the queue with the longest queue obtains the lock first. Fair locks consume a little more performance than unfair locks, but the impact is not great. It is also a good choice to use fair locks in normal development. After all, everyone chooses to pursue fairness 😝.

Release lock

ReentranLock.unlock() code

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

release

    public final boolean release(int arg) {
        if (tryRelease(arg)) { //This needs to be implemented by subclasses
            Node h = head;
            if (h != null &&stat h.waitStatus != 0) //Status cannot be 0 
                unparkSuccessor(h); //Wake up next queue thread
            return true;
        }
        return false;
    }

h.waitStatus = 0 is the status or the default status. We know that the head status is determined by shouldParkAfterFailedAcquire Modified. At this time, the thread is still spinning to acquire the lock, so it does not need to wake up.

tryRelease

ReentranLock's tryRelease is the implementation of fair lock and unfair lock.

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread()) //Release lock thread must be owner thread, otherwise exception will be thrown directly
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) { //State must be equal to 0 to release the lock. It corresponds to the lock re entering state calculation
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

Every time tryRelease() is called, as long as it is not an illegal thread, it can let state - releases, and it will not wake up the thread. As long as state=0, the lock is really released, and the occupation thread is set to null. The waiting thread in the wake-up queue.
A ReentranLock get lock release lock process is finished, but there are many methods in AbstractQueuedSynchronizer. In order to analyze this type, analyze other functions.

lockInterruptibly

When the lock is acquired or queued, the thread can be forced to exit the lock competition. This is the only function of ReentrantLock. The synchronized keyword does not support getting lock interrupt.
ReentrantLock code

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

acquireInterruptibly

Take a look at acquireinterruptible

    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted()) //It has been interrupted, and the interrupt status has been cleaned up
            throw new InterruptedException();
        if (!tryAcquire(arg)) //Failed to acquire lock
            doAcquireInterruptibly(arg); //Failed to list pending
    }

Let's see how the queued thread supports interrupts

doAcquireInterruptibly

    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE); //Join queue current node 
        try {
            for (;;) { //spin
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return;
                }
                // First check the state of the front node and remove the invalid thread
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt()) //Suspend thread returns thread interrupt status
                    throw new InterruptedException();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }

The lock acquisition process is basically the same as the lock method. Before acquiring the lock, the thread interrupt will be judged and it will be processed. In the queued thread, if the thread has been suspended, the interrupt cannot be handled. As long as the thread is awakened, an exception is thrown directly to exit the lock competition.

Acquire lock timeout

ReentrantLock has a method that can be set to acquire the lock within a specified time to avoid the thread being blocked all the time while waiting for the lock to be acquired. By setting the timeout, the caller can handle the failure of lock acquisition. Direct access to code explanation

    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

How to realize AQS

    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) || //Get failed enter the following method
            doAcquireNanos(arg, nanosTimeout);
    }

doAcquireNanos

    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        final long deadline = System.nanoTime() + nanosTimeout; //Time out stamp
        final Node node = addWaiter(Node.EXCLUSIVE); //Join the team
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return true;
                }
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L) { //Has timed out
                    cancelAcquire(node); // Exit queue
                    return false;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD) //Threads larger than 1000 ns need to be suspended. Otherwise, the time is too short, and it will end before the suspension
                    LockSupport.parkNanos(this, nanosTimeout); //Suspend thread, the specified time will be longer than wake-up
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }

According to Baidu's description of nanoseconds, it takes about 2 to 4 nanoseconds for a computer to execute an instruction (such as adding two numbers), so it doesn't make sense if the time interval is too small. This method has more timeout judgment and timeout wake-up threads than the normal method, and the others are the same.

ConditionObject

ConditionObject is an internal class of AbstractQueuedSynchronizer, which implements the main functions of the Condition interface to block threads and wake-up blocking. It is similar to wait, notify and notifyAll. It is generally used to block and wake up threads of producers and consumers of synchronous queues. First, analyze the meaning of each method of the Condition interface, and then analyze the method implementation.

Condition

public interface Condition {

    // Causes the current thread to wait until it receives a signal or an interrupt
    void await() throws InterruptedException;

   //Causes the current thread to wait until it receives a signal
    void awaitUninterruptibly();

    // Causes the current thread to wait until a signal is received or a specified time is exceeded or an interrupt is issued
    long awaitNanos(long nanosTimeout) throws InterruptedException;

    // ditto
    boolean await(long time, TimeUnit unit) throws InterruptedException;

   //The current thread waits until it receives a message, interrupts, or exceeds a specified time
    boolean awaitUntil(Date deadline) throws InterruptedException;

    // Wake up a single waiting thread
    void signal();

    //Wake up all waiting threads
    void signalAll();

Before parsing the implementation class, you need to know the internal properties of ConditionObject

    public class ConditionObject implements Condition, java.io.Serializable {
        /** First node of condition queue. */
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        private transient Node lastWaiter;

        public ConditionObject() { }

There are only two internal attributes, the head node and the tail node. The implementation method is analyzed below.

await

        public final void await() throws InterruptedException {
            if (Thread.interrupted())  //Returns the thread interrupt status and clears the interrupt signal
                throw new InterruptedException();
            Node node = addConditionWaiter();  //Thread enters the waiting queue
            int savedState = fullyRelease(node);  //Perform the lock release function and return the state value
            int interruptMode = 0;
            //  //Whether the node is in the queue  
            while (!isOnSyncQueue(node)) { //Do not suspend thread in queue 
                LockSupport.park(this);  
                //When the thread is waked up, clean up the interrupt state and rejoin the waiting queue
                //When the thread is awakened, judge whether there is an interrupt signal and return it to interruptMode 
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //Spin to acquire lock in the team    
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters(); //Clear thread state is not CONDITION 
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
  1. First, judge whether the thread is interrupted, and then throw an exception directly.
  2. Add Node to the one-way linked list maintained by ConditionObject. This list is mainly used for queue wake-up and does not conflict with the queue for obtaining lock.
  3. Release the lock. I didn't understand it at first. This should be understood in combination with the synchronization queue. When a thread is suspended, it must release the lock, or it will become a deadlock. The producer can't get the lock, insert the data, and wake up the consumer.
  4. When loop processing, as long as the node has joined the thread queue to participate in the lock competition, it will exit the loop or suspend the current thread first. When the thread is awakened, judge whether the thread is awakened due to interruption. If so, jump out of the while loop directly.
  5. Now that you have joined the queue, you can get the lock.
  6. At this time, the node node has acquired the lock waitStatus, which has changed. It is necessary to clear the illegal nodes in the one-way linked list, including itself.
  7. When interruptMode is not equal to 0, it means that there is an interrupt that needs to be handled by the caller himself.

addConditionWaiter

        private Node addConditionWaiter() {
            if (!isHeldExclusively()) //Not the current lock owner
                throw new IllegalMonitorStateException();
            Node t = lastWaiter;
            // If the lastWaiter is not in the CONDITION state, remove the queue, and the CONDITION is the exclusive state of the synchronization queue
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();  //Remove nextWaiter queue with status not equal to CONDITION
                t = lastWaiter;
            }

            Node node = new Node(Node.CONDITION); //Create a node and set waitStatus to CONDITION

            if (t == null) //The header node has not been initialized
                firstWaiter = node;
            else
                t.nextWaiter = node; //node is either the head or the tail
            lastWaiter = node; 
            return node;
        }


In conditionioinobject, a one-way condition list connected by nextWaiter will be maintained. The main features of this list are as follows Node.waitStatus Must be Node.CONDITION , will put the head node and tail node of the list into the lastWaiter and firstWaiter nodes.

unlinkCancelledWaiters

        private void unlinkCancelledWaiters() {
            Node t = firstWaiter;
            Node trail = null;
            while (t != null) { //Traverse from the beginning
                Node next = t.nextWaiter;
                if (t.waitStatus != Node.CONDITION) {
                    t.nextWaiter = null; //Cut off the association between t and subsequent nodes, which is easy to delete below
                    if (trail == null)  //Delete the head node. The next node is the head node
                        firstWaiter = next;
                    else 
                        trail.nextWaiter = next; 
                    if (next == null)
                        lastWaiter = trail;
                }
                else
                    trail = t;
                t = next;
            }
        }

Traverse the entire condition list, and Node.waitStatus ! Node.CONDITION Delete.

isOnSyncQueue

    final boolean isOnSyncQueue(Node node) {
        if (node.waitStatus == Node.CONDITION || node.prev == null) //node has just released the lock and has not entered the queue once
            return false;
        if (node.next != null) //The front node is not empty, it must be in the queue
            return true;
        return findNodeFromTail(node); //Find node from tail and return true
    }

checkInterruptWhileWaiting

        private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0;
        }

Check for thread breaks and clean it up. Return 0 without interruption, and transferaftercanceledwait will not be executed. When interrupted, the transferaftercanceledwait is executed and an interrupt structure is returned.

  • REINTERRUPT interrupt exit while waiting for exit
  • THROW_IE throws an InterruptedException exception to exit

transferAfterCancelledWait

    final boolean transferAfterCancelledWait(Node node) {
        if (node.compareAndSetWaitStatus(Node.CONDITION, 0)) { //Waitstatus condition = > 0 set successfully
            enq(node); //Join queue
            return true;
        }

        while (!isOnSyncQueue(node)) //node is not in the queue
            Thread.yield(); //Thread gives up execution right and waits for node to enter the queue and then jumps out of spin
        return false;
    }

According to the previous code for obtaining lock, waitStatus is the default value of 0. Only when aitstatus condition = > 0 is set successfully can we join the queue and participate in lock competition. If the setting fails, it will spin to determine whether the node is in the queue, and it will exit only when it is in the queue.

reportInterruptAfterWait

        private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            if (interruptMode == THROW_IE) // Throw the interrupt exception again and hand it to the caller for handling
                throw new InterruptedException();xin
            else if (interruptMode == REINTERRUPT)  //Interrupt exit on exit
                selfInterrupt(); //Call thread interrupt
        }

According to different interruptions, different treatments are made.

signal

        public final void signal() {
            if (!isHeldExclusively()) //Must be exclusive lock thread execution
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null) //The header node is not empty, indicating that the condition queue exists and can be waked up
                doSignal(first); //Wake up thread
        }

See how the wake-up thread is implemented

doSignal

        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)  //The next node is empty, and the list is finished
                    lastWaiter = null;
                first.nextWaiter = null;
                //This method is the main wake-up logic
            } while (!transferForSignal(first) && 
                     (first = firstWaiter) != null);
        } 

transferForSignal

    final boolean transferForSignal(Node node) {
        if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))  //Join queue waitStatus=0
            return false;
        //Join queue competition lock
        // p is the node front node
        Node p = enq(node); 
        int ws = p.waitStatus;
        // If ws is greater than 0 P, it will exit the queue and wake up the post node
        // p failed to set the state. The p node can no longer exist. At this time, the thread should be awakened to acquire the lock.
        if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
            LockSupport.unpark(node.thread); //Wake up node's thread
        return true; //Wake up success returns true 
    }

Here, the condition of wake-up thread is whether the front node is available, as long as the front node is ready to exit the queue or has been deleted. The node is awakened to acquire the lock.
I'm not going to write the rest of the methods. There's too much content. I'm interested in learning by myself. The content is almost repetitive.

summary

This paper analyzes the internal source code principle of AbstractQueuedSynchronizer through the principle of obtaining lock releasing lock, and also analyzes the difference between fair lock and unfair lock. Explain the locks of some derivative functions, such as timeout lock and interrupt lock. It completely analyzes AbstractQueuedSynchronizer as the framework support of Java ReentrantLock, and simply analyzes ConditionObject's thread coordination signal and await methods. Most of the content of this article is my own thinking. If there are any mistakes, please come out and discuss with us.

Topics: Java REST