Source code analysis of JUC-AQS

Posted by israely88 on Tue, 21 Dec 2021 20:27:58 +0100


Pre knowledge

  • CAS
  • Reentrant lock
  • LockSupport

1, Introduction

AbstractQueuedSynchronizer is the basic framework used to build locks and other synchronization components. It uses an int member variable to represent the synchronization status and completes the queuing of resource acquisition threads through the built-in FIFO. Common APIs such as CountDownLatch, CyclicBarrier and Semaphore involve lock control and inherit AQS.

The main method used by the synchronizer is inheritance. Subclasses manage the state by inheriting the synchronizer and implementing its abstract methods. In this process, they will control the change of state

2, Important elements of AQS

state: Lock occupancy status code. 0 means it is not occupied
Head and tail represent the head and tail of AQS queue
Among them, there is a static internal class Node, which is used to encapsulate threads

3, Scenario:

There is only one window (one lock) for the bank to handle the business. Three customers a, B and C come to handle the business. A first comes to the window to handle the business for a (it takes a long time), so B and C wait for a in the waiting area arranged by the bank

		ReentrantLock lock=new ReentrantLock();
        new Thread(()->{
            lock.lock();
            try {
                try {
                    TimeUnit.MINUTES.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                lock.unlock();
            }
        },"A").start();

        new Thread(()->{
            lock.lock();
            try {
              
            } finally {
                lock.unlock();
            }
        },"B").start();

        new Thread(()->{
            lock.lock();
            try {

            } finally {
                lock.unlock();
            }
        },"C").start();
	public void lock() {
        sync.lock();
    }

4, Source code analysis

The acquisition lock and release lock of unfair lock are used for analysis

1. lock method of realizing sync by NonfairSync

		final void lock() {
            if (compareAndSetState(0, 1))//If the lock is not occupied, change the occupied state 1
                setExclusiveOwnerThread(Thread.currentThread());//Sets the current thread as a consuming thread
            else
                acquire(1);
        }

A first window
compareAndSetState(0, 1), because the lock is initially in the unoccupied state (state is 0), it is replaced with 1 after the comparison is the same (indicating that the lock is occupied)
The comparison and replacement successfully executed setExclusiveOwnerThread(Thread.currentThread()), and set the occupied thread as the current thread (lock occupied)
Then B comes
compareAndSetState(0, 1). Because the lock is occupied and the state is 1, the comparison fails. Execute acquire(1)

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

1.1. NonfairSync implements the tryAcquire method of AbstractQueuedSynchronizer

		protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
		final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();//Get occupancy status
            //For the situation where A has just ended
            if (c == 0) {//Not occupied, trying to get lock
                if (compareAndSetState(0, acquires)) {//Replace occupancy status code
                    setExclusiveOwnerThread(current);//Set occupied thread
                    return true;//Success is returned, and failure returns the lowest false
                }
            }
            //It has been occupied. Judge whether the current thread is a lock occupied thread
            //If so, you can re-enter
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;//accumulation
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);//Set occupancy status code
                return true;
            }
            return false;
        }

An unfair lock first attempts to acquire the lock
Get the successful replacement status code and set the occupied thread, and return true

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

Thread B attempts to acquire lock successfully and exits directly

Failed to get. Judge whether the current thread is an occupied thread
If yes, the reentrant status code can be accumulated
No, return false

1.2. Acquirequeueueued (addwaiter (node. Exclusive), Arg) is the next condition of acquire

If the attempt to acquire the lock fails or the reentry fails, judge the next condition acquirequeueueueueueueueued (addwaiter (node. Exclusive), Arg)
C is also the same. Because it is an unfair lock, try to obtain and judge the reentrant lock first

	public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
1.2. 1. addWaiter method of AbstractQueuedSynchronizer
    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) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

Node encapsulates the B node of the B thread
Node pred = tail; The current AQS queue is empty
pred == null, so execute enq(node) and add node B

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

spin
Node t = tail; Get that the tail node is empty, enter if, and judge compareAndSetHead(new Node())

    private final boolean compareAndSetHead(Node update) {
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }

Compare whether the head node of AQS queue is empty. If yes, update it to new Node();
This node is a sentinel node
Compare and replace the head node successfully, and the tail node refers to the head node

Continue spin
It is found that the tail node is a sentinel node, which is not empty. Enter else

			else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }

Node is node B
The leading node of node B points to the trailing node (currently sentinel)

compareAndSetTail(t, node)

	private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

Compare whether the tail node is the tail node and replace it with B node

Compare and replace successfully t.next=node

C thread enters the same

Finally return to the sentinel node and exit the spin

1.2.2,acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

addWaiter returns the sentinel node and assigns it to node. arg is 1

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();//Get front node
                if (p == head && tryAcquire(arg)) {//Whether it is a header node and attempts to obtain a lock
                    setHead(node);//Reclaim the sentinel node, and then change the next node of the sentinel node to sentinel
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
		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;

spin
The front node of the sentinel node is the head node. If the attempt to obtain the lock is successful, the spin ends directly and returns false

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

The thread ends directly after obtaining the lock
If the acquisition fails

1.2.2.1,shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()

p is the head node and node is the sentry

	 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;//Obtain the waiting code. All nodes default to 0
        if (ws == Node.SIGNAL)//If waiting for resource - 1
            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;
    }

The node waitStatus defaults to 0. Enter else to execute compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

    private static final boolean compareAndSetWaitStatus(Node node,
                                                         int expect,
                                                         int update) {
        return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                        expect, update);
    }

Compare whether it is a sentinel node and replace waitStatus with node Signal (wait for resource code, indicating access blocking)

After comparison and replacement, the result is returned as false, if ends and continues to spin

		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;

Determine the head node again and try to obtain the lock
If it fails, continue shouldparkafterfailedacquire (P, node) & & parkandcheckinterrupt()

	 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;//Obtain the waiting code. All nodes default to 0
        if (ws == Node.SIGNAL)//If waiting for resource - 1
            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;
    }

However, after entering this time, the waitStatus has been changed to node Signal, return true and enter the next judgment parkAndCheckInterrupt()

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

This method formally blocks the B thread, waits for wake-up (unpark release) or interrupts, and returns from the park
If you don't wake up, you've been waiting here

2. When thread A finishes executing, release the lock

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

2.1 tryRelease of sync

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;//Occupancy status code 1-1 = 0
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);//Release succeeded. Set the occupied thread to null
            }
            setState(c);//And change the occupation code to 0 (indicating that there is no thread occupation)
            return free;
        }

Attempt to release
Release succeeded. Set the occupied thread to not empty, and change the occupation code to 0 (indicating that no thread is occupying the lock)

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

The attempt to release is successful. Continue
The head node is the sentinel node
Sentinel node waitStats=-1, execute unparksuccess (H)

private void unparkSuccessor(Node node) {

        int ws = node.waitStatus;//-1
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);//Change to 0

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

Sentinel waitStatus=-1, compare and replace with 0


Node s = node. Next = node B
B!=null,B.waitStatus=0

	if (s != null)
            LockSupport.unpark(s.thread);

2.2. Wake up thread B and return park

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
	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);
        }
    }

Continue spin
The sentinel is the head node, and the attempt to obtain the lock is successful (because the lock has been released, status = 0, setExclusiveOwnerThread(null))

2.3. Retrieve the sentry and modify the lock node

Node B obtains the lock, GC reclaims the sentry, and modifies node B as the sentry

		setHead(node);
        p.next = null; // help GC
        failed = false;
        return interrupted;
    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }


Exit spin, C continues to wait as before

5, Summary

Unfair lock
1. Perform lock locking. Try to occupy it directly first. If not, acquire it
2. Acquire attempt to acquire lock tryAcquire
Lock acquisition successful lock exit (modify occupation code to occupation, set occupation thread not to current thread)
Failed to acquire the lock. Encapsulate the thread node and add it to the AQS queue
2. If the queue is empty, first create the sentinel node, and then insert the thread node behind the sentinel
If the queue is not empty, add it directly later
3. After adding the queue, modify the sentinel node waitStatus to SIGNAL, and use the node thread locksupport Park is blocked, waiting for the occupying thread to wake up using unpark
4. unlock to release the lock, try to release the lock
Release succeeded. Modify the occupation code to unoccupied, and set the occupation thread to not empty
5. Released successfully. Modify the sentinel node waitStatus
6. Wake up the threads of subsequent nodes through the sentinel node
7. After waking up, recover the sentinel node and modify the subsequent node to sentinel

Fair lock
Direct acquire

Topics: Multithreading