Combined with JDK8 source code, this paper deeply analyzes the implementation principle of AQS and ReentrantLock

preface

This article belongs to the column "100 problems to solve Java concurrency". This column is original by the author. Please indicate the source of quotation. Please help point out the deficiencies and errors in the comment area. Thank you!

Please refer to table of contents and references for this column 100 problems to solve Java concurrency

text

Original design intention of AQS

Doug Lea once introduced the original design intention of AQS.

Doug Lea, needless to say, you can search the source code of JDK. He developed the merger contract of JDK5. It's a real Java God.

In principle, a synchronization structure can often be implemented by other structures. However, the tendency to a certain synchronization structure will lead to complex and obscure implementation logic. Therefore, he chose to abstract the basic synchronization related operations in AbstractQueuedSynchronizer and use AQS to provide a model for us to build a synchronization structure.

AQS source code (JDK8)

4 cores

state

An integer member of volatile represents the state, and provides both setState and getState methods

    /**
     * The synchronization state.
     */
    private volatile int state;

queue

A first in first out (FIFO) waiting thread queue to realize multi-threaded competition and waiting, which is one of the cores of AQS mechanism.

    /**
     * Wait for the head of the queue and delay initialization.
     * Except for initialization, it is only modified by the method setHead.
     * Note: if the head exists, ensure that its waitStatus is not canceled
     */
    private transient volatile Node head;

    /**
     * Wait for the end of the queue to delay initialization.
     * Add a new wait node only by method enq modification.
     */
    private transient volatile Node tail;

As can be seen from the source code, AQS implements FIFO queue through double linked list

CAS

Various CAS based basic operation methods

    /**
     * If the current status value is equal to the expected value, the synchronization status is automatically set to the given update value.
     * This operation has memory semantics of volatile read and write. 
     * Parameter: expect – expected value 
     * 			update–New value 
     * Return: true if successful. A false return indicates that the actual value is not equal to the expected value.
     */
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

    /**
     * CAS head field. Used only by enq.
     */
    private final boolean compareAndSetHead(Node update) {
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }

    /**
     * CAS tail field. Used only by enq.
     */
    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

    /**
     * CAS waitStatus field of a node.
     */
    private static final boolean compareAndSetWaitStatus(Node node,
                                                         int expect,
                                                         int update) {
        return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                        expect, update);
    }

    /**
     * CAS next field of a node.
     */
    private static final boolean compareAndSetNext(Node node,
                                                   Node expect,
                                                   Node update) {
        return unsafe.compareAndSwapObject(node, nextOffset, expect, update);
    }

It can be seen from the source code that the object of CAS operation is state or double linked list Node.

acquire/release

Various acquire/release methods that are expected to be implemented by specific synchronization structures

    /**
     * Get in exclusive mode, ignoring interrupts.
     * This is achieved by calling tryAcquire at least once and returning when successful. Otherwise, the thread will			  
     * Queuing, blocking and unblocking may be repeated, and tryAcquire may be called until successful.
     * This method can be used to implement the Lock.Lock method.
     *  Parameter: arg – acquire parameter. This value is passed to tryAcquire, but will not be interpreted in other ways, and can
     * Indicate anything you like.
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

    /**
     * Publish in exclusive mode. If tryRelease returns true, it is implemented by unblocking one or more threads. This method can
     * Used to implement the method Lock.unlock. 
     * Parameter: arg – release parameter. This value will be passed to tryRelease, but it is not strange in other aspects. It can indicate your preference
     * Anything. 
     * Return: the value returned from tryRelease
     */
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

To implement a synchronization structure using AQS, at least two basic types of methods should be implemented: acquire operation to obtain the exclusive right of resources; There is also the release operation, which releases the exclusive right to a resource.

ReentrantLock

Take ReentrantLock as an example. It internally implements the Sync type by extending AQS, and uses the AQS state to reflect the lock holding.

    /**
     * This lock is the basis of synchronization control. It is divided into the following fair and unfair versions.
     * The state of AQS is used to represent the number of reservations on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {...}

The following are the acquire and release operations corresponding to ReentrantLock.

lock

    /**
     * Get the lock. 
     * If the lock is not held by another thread, acquire the lock and return immediately, setting the lock hold count to 1. 
     * If the current thread already holds the lock, the hold count is incremented by 1 and the method returns immediately. 
     * If the lock is held by another thread, the current thread will be disabled for thread scheduling purposes and will be in a sleep state until
     * Until the lock is obtained, the lock holding count is set to 1. 
     */
    public void lock() {
        sync.lock();
    }

The lock of ReentrantLock calls the lock method of Sync internally

    /**
     * Performs {@link Lock#lock}. The main reason for subclassing
     * is to allow fast path for nonfair version.
     */
    abstract void lock();

Sync is an abstract class. Lock is implemented by its two subclasses fair lock: fairsync and unfair lock: NonFairSync

FaireSync.lock

    final void lock() {
          acquire(1);
      }

NonFairSync.lock

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

In ReentrantLock, the tryAcquire logic is implemented in NonfairSync and FairSync to provide further unfair or fair methods respectively, while the tryAcquire in AQS is only a method close to the unimplemented method (throwing exceptions directly), which is left to the implementer to define

FaireSync.tryAcquire

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    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;
    }
}

NonFaireSync.tryAcquire

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }


    /**
     * Performs non-fair tryLock.  tryAcquire is implemented in
     * subclasses, but both need nonfair try for trylock method.
     */
    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;
    }

The unfair tryAcquire internally implements how to cooperate with the state and CAS to obtain the lock. Note that compared with the fair version of tryAcquire, it does not check whether there are other waiters when the lock is unoccupied. This reflects the unfair semantics.

acquireQueued

    /**
     * The thread already in the queue gets in exclusive uninterrupted mode.
     * Method and acquisition for conditional waiting.
     * Parameter: node – node 
     * 	arg–acquire parameter 
     * Return: true if interrupted while waiting 
     */
    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 the previous tryAcquire fails, it means that the lock contention fails and enters the queuing competition stage.
Here is what we call the part of using FIFO queue to realize lock competition between threads. It can be regarded as
The core logic of AQS.
The current thread will be wrapped as an EXCLUSIVE node through the addWaiter method
Add to queue.
The logic of acquirequeueueueueueueueueued, in short, is to try to get a header node if the current node is preceded by a header node
Take the lock, and if everything goes well, it will become a new head node;
Otherwise, wait if necessary.

Here, the process of threads trying to obtain locks is basically shown. tryAcquire is a part that developers need to implement according to specific scenarios, and inter thread competition is provided by AQS through Waiter queue and acquirequeueueueueueueueueueued. In the release method, the queue will also be operated accordingly.

unlock

	/**
	 * An attempt was made to release the lock. 
	 * If the current thread is the holder of this lock, the hold count is reduced.
	 * If the hold count is now zero, release the lock.
	 * If the current thread is not the holder of this lock, an IllegalMonitorStateException is thrown. 
	 * Throw: IllegalMonitorStateException - if the current thread does not hold this lock
	 */
    public void unlock() {
        sync.release(1);
    }

 	/**
     * Publish in exclusive mode.
     * If tryRelease returns true, it is implemented by unblocking one or more threads.
     * This method can be used to implement the method Lock.unlock. 
     * Parameter: arg – release parameter. This value will be passed to tryRelease, but it is not strange in other aspects. It can indicate your preference
     * Anything. 
     * Return: the value returned from tryRelease
     */
    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;
    }

Tags: Java Concurrent Programming

Posted on Mon, 20 Sep 2021 01:53:41 -0400 by wizardry