[Java] AQS Source Code Analysis

Keywords: Hadoop Java JDK Big Data

The full name of AQS is AbstractQueued Synchronizer, which is a synchronizer design framework provided by JDK. Many concurrent data structures, such as ReentrantLock, ReentrantReadWriteLock, Semaphore and so on, are implemented based on AQS. Next, the principle of AQS is analyzed.

 

I. underlying data structure

The AQS underlying layer maintains a state (representing shared resources) and a CLH queue (waiting queue for threads)

State: state is a volatile int variable that represents the use of shared resources.

Head, tail: Represents the head and tail nodes of the CLH queue, respectively. See this blog about CLH queuing principles:

private transient volatile Node head;

private transient volatile Node tail;

private volatile int state;

AQS defines two resources:

One is Exclusive, which is accessible by only one thread at a time, such as ReentrantLock.

One is Share, which allows multiple threads to access at the same time, such as Semaphore.

 

II. AQS Principle

AQS is defined as a synchronizer framework, so why is AQS a "framework"? The definition of the framework should be that the user implements some business logic according to the requirements of the framework, and then the bottom layer is completed by the framework itself. For example, when using Hadoop big data framework, we just need to define Map/Reduce function, and other complex underlying operations are Hadoop. Help us finish. AQS should also be such a thing, in fact, AQS does, it only requires developers to implement several of these functions, other underlying operations such as CLH queue operations, CAS spin and so on are implemented by the AQS framework.

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}

protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}

The above methods are the functions that AQS wants us to implement. These methods are provided by AQS for developers to write code logic. Therefore, each method is defined as throwing an Unsupported OperationException directly. Here's how these functions work.

tryAcquire: Try to get exclusive resources. Successful return true, failure return false.

tryRelease: Try to release exclusive resources. Successful return true, failure return false.

tryAcquireShared: Try to get shared resources. Successful return of a non-negative number represents the remaining resources (0 means no available resources), and negative number means failure.

Try Release Shared: Try to release shared resources. Successful return true, failure return false.

isHeldExclusively: Determines whether threads are monopolizing resources.

For an exclusive synchronizer, only tryAcquire-tryRelease is needed; for a shared synchronizer, only tryAcquire Shared-tryRelease Shared is needed. Of course, if a custom synchronizer needs to implement both exclusive and shared modes, it can also, for example, read-write lock ReentrantReadWriteLock, where read-write lock is shared and write-lock is exclusive.

 

3. Source code parsing

The top-level interfaces for resource acquisition in AQS are acquire-release and acquireShare-release Share.

1,acquire

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

Code logic:

(1) Attempt to acquire resources (try Acquire), success returns, failure jumps to (2)

(2) Requests that the thread be added to the end of the thread waiting queue and marked as addWaiter(Node.EXCLUSIVE), and constantly requests queue wisdom to obtain resources, if the process is interrupted, jump to (3), and return if it fails.

(3) Self Interruption

(4) Completion

 

2,release

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

Code logic:

(1) Attempt to release resources, jump to (2) if successful, and return false if unsuccessful.

(2) Get the head node of the thread waiting queue, and wake up the thread (unpark Successor) if the head node thread is not empty and in a waiting state.

 

3,acquireShared

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

Code logic:

(1) Attempt to obtain shared resources, and call the doAcquireShared spin to request shared resources if acquisition fails.

 

4,releaseShared

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

Code logic:

(1) Attempt to release shared resources, and successful release awakens successor nodes in the waiting queue to obtain resources.

 

5. AQS also provides acquireInterruptibly and acquireSharedInterruptibly functions to respond to interrupts.

 

4. Practical examples: ReentrantLock parsing

Generally, AQS is used to implement synchronizer mainly in the way of internal classes. ReentrantLock is a more classical implementation. Look at the source code of this internal class.

    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        @ReservedStackAccess
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                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;
        }

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

        protected final boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        final ConditionObject newCondition() {
            return new ConditionObject();
        }

        final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }

        final int getHoldCount() {
            return isHeldExclusively() ? getState() : 0;
        }

        final boolean isLocked() {
            return getState() != 0;
        }

        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }

The Sync synchronizer class defined here is still an abstract class, because ReentrantLock supports both fair and unfair locks, and the two synchronization methods are different. So we first define a Sync class as the base class of the two implementations, and then we will see the implementation of specific fair and unfair locks.

Since ReentrantLock itself is an exclusive lock, the Sync class mainly overrides the tryRelease and isHeldExclusively methods in AQS and defines the nonfairTryAcquire method of unfair locks as a method of requesting resources.

 

Unfair lock implementation:

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

In fact, the base class Sync is completely an implementation of an unfair lock, so there is no new code for NonfairSync, tryAcquire calls nonfairTryAcquire directly.

 

Fair Lock Implementation:

    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 difference between fair locks and unfair locks is that resource requests are different, so the tryAcquire method is rewritten on the basis of Sync-like to wake up the threads in the waiting queue sequentially.

 

After realizing fair lock and unfair lock based on AQS, how to realize the function of locking and unlocking is very simple. The source code can be realized with one line of code directly.

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

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

 

Posted by giraffemedia on Wed, 15 May 2019 16:13:37 -0700