Take ReentrantLock as an example to explain the underlying principle of AQS in detail

Posted by eddieblunt on Mon, 27 Dec 2021 07:40:11 +0100

AQS

Reentrant lock

When the thread that has successfully acquired the lock obtains the lock again, it can be count + +. As long as count > 0 means that the lock is held by the thread, release the lock every time count - until count=0 means that the thread no longer holds the lock; Reentrant lock can effectively avoid deadlock of a single thread.

LockSupport

LockSupport is the basic thread blocking primitive used to create locks and other synchronization classes;

The park() and unpark() methods are used to block threads and unblock them.

wait() in synchronized realizes thread waiting, and notify() realizes thread wake-up; You must wait before waking up;

In lock, condition await() implements thread waiting, and signal() implements thread wake-up; You must wait before waking up;

park() and unpark() in LockSupport are their upgraded versions with stronger functions.

Each thread will have a permission (0 / 1). If LockSupport.park(), the permission is 0, indicating that the current thread is blocked; If LockSupport Unpark(), if permit is 1, it means that the current thread is awakened; The permit for multiple calls to park() can only be 0, and the permit for multiple calls to unpark() can only be 1; The advantage of LockSupport is that it does not need to syn c code blocks or lock to unlock. You can directly use these two functions.

AbstractQueuedSynchronizer Abstract queue synchronizer

The above two are the pre knowledge of AQS. Now analyze AQS from the source code.

AQS is the basic framework for realizing various synchronous locks and the cornerstone of the whole JUC system.

Internal structure: int state indicates the number of resource occupiers + CLH two-way queue (threads that cannot obtain locks temporarily will enter the queue and wait)

State uses volatile modification to indicate the synchronization status, completes the queuing of resource acquisition through the built-in FIFO queue, encapsulates each thread to preempt resources into a Node node to realize lock allocation, and modifies the state value through CAS.

static final class Node {
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;

    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;

    static final int PROPAGATE = -3;

    volatile int waitStatus;

    volatile Node prev;

    volatile Node next;

    volatile Thread thread;


    Node nextWaiter;

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

    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {    // Used to establish initial head or SHARED marker
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

All locks are actually rewriting methods such as tryAcquire(), tryRelease() in AQS (template mode)

Take ReentrantLock as an example to understand AQS

AQS is used as the internal code of a template. It is meaningless to analyze the internal code, so directly select one of its implementation classes:

ReentrantLock has a member variable sync inside, which inherits AQS and is constructed as an unfair lock by default.

Fair lock and unfair lock

hasQueuedPredecessors() is a method to judge whether there are valid nodes in the waiting queue when a fair lock is locked.

Take the three threads of ABC as an example. When thread A enters, the lock is not used, and the state is 0, then cas(0,1) succeeds. Directly change the thread of exclusive lock to itself, and then start business logic.

At this time, thread B comes and performs CAS(0,1) first. If it fails all the time, it enters the acquire() method:

The tryAcquire() method was called first,

The internal function is nonfairtryacquire()

At this time, judge the state again (in case you don't grab it just now, you have a chance). If you can grab it, it will become a thread of mutex lock. Otherwise, judge whether you can re-enter the lock first, and if so, you can occupy the lock; otherwise, even if you fail to get the lock;

After the lock retrieval fails, enter the addWaiter(Node.EXCLUSIVE) method

At this time, the tail node in the CLH will be obtained. If the tail node is empty, the enq() method will be used to initialize the queue and generate a virtual head node (also known as dummy node and sentinel node). waitStatus=0 of the virtual node represents the initial value, Thread=null represents that the virtual node has no thread Association, and the node generated by thread B with waitStatus=0 will be placed at the tail of the queue.


At this time, the first two conditions of if() in Acquire()! Tryacquire & & addwaiter() has been done. This time, B gives up and can only enter the queue for waiting, that is, acquirequeueueueueueueueueed() method

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            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);
    }
}

If pre is the head node, it means that it is already in the queue head. At this time, try to obtain the lock again:

If tryAcquire() succeeds, use setHead() to release the virtual header node, set yourself as the header node, change the thread to null, and modify the pointer; Then, the pointers of virtual head nodes are all null to facilitate GC recovery;

Otherwise, the core is shouldParkAfterFailedAcquire();

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

After entering this function, you will first see the status of the previous node of this node. If it is in SIGNAL status, it means that the previous node will notify itself and return true when resources are released; If > 1 indicates that the status of this pre node is CANCEL, then the previous node will be eliminated; Otherwise, set the previous node to the SIGNAL state.

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

Then use the LockSupport() method to block yourself here (don't waste CPU grabbing, quickly block yourself and wait); When it is unpark(), it will continue to go down;

After a while, C also came. He also passed the tryAcquire() method and didn't grab anything. He can only call addWaiter(). At this time, the CLH queue has been initialized, so he directly uses cas to join the team; After the above series of operations, C is also blocked here;

Here, the process of locking and joining the team is over, so it is easier to release the lock:

Unlock() call

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free

cas the state. If it is 0, a true identifier is returned;

If the head node is a virtual node and the state is not the initial value, it means that someone is waiting behind, and unpark() is performed to unblock him

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) {
        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);
}

Unparkwinner () changes the state of node to 0, releases the virtual node, and turns node into a head node.

Topics: Java Interview