Interpretation of AQS source code

AQS

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable ...

AQS, AbstractQueuedSynchronizer (Abstract queue synchronizer), is an abstract class of other synchronizers
It inherits an AOS (AbstractOwnableSynchronizer), which is only used to save threads that hold locks

Some important attributes

Some properties and internal classes of AbstractQueuedSynchronizer are as follows:

//Queue header of synchronization queue
private transient volatile Node head;
//End of synchronization queue
private transient volatile Node tail;
//The meaning of status value is related to the specific implementation class
private volatile int state;
//Related to Condition, accurate wake-up can be realized
public class ConditionObject{
	//Queue header of condition queue
	private transient Node firstWaiter;
	//End of condition queue
	private transient Node lastWaiter;
	
}
//Each node of the queue corresponds to a thread
static final class Node {
	// Represents shared mode and exclusive mode respectively
	static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;
    // Indicates that the current node task has been cancelled
    static final int CANCELLED =  1;
    // Indicates that the next thread is waiting to wake up
    static final int SIGNAL    = -1;
    // Indicates that the current node is a conditional node,
    static final int CONDITION = -2;
    // Indicates that releaseShared should be propagated to other nodes
    static final int PROPAGATE = -3;
    // Wait state, the initial value is 0, or the above value can be taken
    volatile int waitStatus;
    // Represents the previous node and the latter node respectively
    volatile Node prev;
    volatile Node next;
    //Thread object corresponding to Node
    volatile Thread thread;
    // Point to the next node corresponding to the special Condition object or SHARED node
    Node nextWaiter;
}

Template method of AQS

AQS class defines several template methods to be overridden by subclasses. As you can see, AQS provides exclusive (exclusive) and non exclusive (shared) modes. You only need to rewrite the modes that need to be used.

//Try to acquire the lock object in exclusive and shared mode respectively
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}
//Attempt to release the lock object in exclusive and shared mode respectively
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}
//Returns whether the current thread holds the lock in exclusive mode
protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}

Several important methods in AQS

acquire lock

Start with acquire

	// Attempt to exclusive lock object
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

tryAcquire(arg) is overridden by subclasses and will not be analyzed temporarily
Now analyze addWaiter(Node mode) and acquirequeueueueueed (final node, int ARG) respectively

	// Add to waiting queue and return
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // If a waiting queue already exists, you can add it directly. Otherwise, you need to build a waiting queue first
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
    // Initialize the waiting queue. An empty node will be formed at the head node, and then add a node node to the queue
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
    // After joining the waiting queue, if the conditions are met, you can try to obtain the lock object again, or block if you can't obtain it and wait for wake-up
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                // If it is the successor node of the head node, you can try to obtain the lock object again (in fact, it should be twice. See the explanation below)
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //Judge whether it should be blocked, and if so, block
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

Next, analyze shouldParkAfterFailedAcquire(Node pred, Node node), parkAndCheckInterrupt() and cancelAcquire(Node node)
For shouldParkAfterFailedAcquire(Node pred, Node node) method, when each node is called for the first time, the waitStatus of the front node is 0. Therefore, false will be returned for the first time, and if (P = = head & & tryacquire (ARG)) in acquirequeueueueueueuented (final node, int ARG) method will run twice before blocking.

	// To return whether the current thread should be blocked (determined by the waitStatus of the preceding Node), you can see the explanation of the Node attribute at the beginning of this article
	private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        // Returns true if the waitStatus of the front node is SIGNAL(-1)
        if (ws == Node.SIGNAL)
            return true;
        // If > 0 (in fact, it is 1, which means cancelled), the cancelled node is skipped
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    // Block the thread and return whether to interrupt
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
    // Cancel node task
    private void cancelAcquire(Node node) {
        if (node == null)
            return;
        // The thread in the canceled node is null
        node.thread = null;
        Node pred = node.prev;
        // Find a non cancelled node forward and record it as PRED. The precursor pointer of the modified node points to pred
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        Node predNext = pred.next;
        // Change state
        node.waitStatus = Node.CANCELLED;
        // If node is the tail node, set pred as the tail node
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            int ws;
            // If pred is not a header node, set its waitStatus to SIGNAL. If the thread of pred is not empty, proceed to the next step
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                // If the post node of node is not empty and has not been cancelled, set the post node of pred as the next node
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
            	// If pred is the head node, wake up the successor node
                unparkSuccessor(node);
            }
            node.next = node; // help GC
        }
    }

release lock

Now, analyze the process of releasing the lock
Start with release

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            // If the header node is empty or the waitStatus of the header node is 0, skip the unparksuccess (H) method
            return true;
        }
        return false;
    }

tryRelease(arg) is overridden by subclasses and will not be analyzed temporarily
We further analyze the unparkwinner (node node) method

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        // If the current node is not cancelled, set waitStatus to 0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        // If the successor node is empty or cancelled, start from the end of the queue to find a non node node that has not been cancelled
        if (s == null || s.waitStatus > 0) {
            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);
    }

Therefore, after the release method releases the lock, if there is a next node and the next node is not cancelled, the thread corresponding to the next node will be awakened
If there is a next node, but the next node is cancelled, start from the end of the queue to find a node that has not been cancelled and wake up the corresponding thread

Specific applications include ReentrantLock, ReentrantReadWriteLock, CountDownLatch, CyclicBarrier, Semaphore, etc.
When learning the above categories, we have two concerns
① Meaning of state field in AQS
② Subclass overrides template methods in AQS

ReentrantLock

ReentrantLock is a reentrant lock. It rewrites three template methods and realizes two exclusive locks: fair and unfair.
FairSync and NonfairSync both inherit Sync, and Sync inherits AQS

Reentrant bottom layer

① The state field indicates the number of layers occupied by the current thread lock (reentrant lock and reentrant key)
This value is + 1 for each layer; The value is - 1 for each layer
When it is 0, it means that there is no thread occupation
See below for codes

Fair and unfair

View key codes directly

		// Attempt to acquire locks fairly
		protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
            	// Here we will judge whether there is a waiting queue, which is the embodiment of fairness
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    // This method is in AOS (briefly introduced at the beginning of this article) and is used to set the thread object in it
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // Reentrant key
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
        // Unfair attempt to acquire lock
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
            	// Note the difference here
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

Other applications will not be considered for the time being.

Recommended reading

The java.util.concurrent Synchronizer Framework JUC synchronizer framework (AQS framework) original translation

Tags: Java

Posted on Wed, 03 Nov 2021 13:42:24 -0400 by caine