AbstractQueuedSynchronizer AQS Lock Principle and ReentrantLock Unfair Lock Implementation

Keywords: Java

AbstractQueuedSynchronizer is based on a FIFO two-way Chain queue==CLH queue==, a class used to build locks or synchronizers, also known as Java synchronizers. The equitable and unfair locks of ReentrantLock consist of the synchronizers, and the chain queue structure diagram is as follows.

You can understand that bank ATM machine withdraws money, one person goes to get the lock first, during which time other threads are blocked. Only when he finishes collecting money, he goes away and releases the lock. Only the people behind him can get the released lock and withdraw money.

The synchronizer uses an int value to represent the state by ==inheriting the synchronizer using an internal class==implementing methods such as tryRelease and tryAcquire to manage the state, using the following three methods:

  • getState() Gets the state
  • setState() Basic setup state
  • compareAndSetSate(int,int) atomicity setting state based on CAS implementation

AQS Node

Nodes contain the following states:

  1. CANCELLED, with a value of 1, indicates that the current thread has been cancelled;
  2. SIGNAL, with a value of -1, indicates that the successor nodes of the current node contain threads that need to be run, that is, unpark;
  3. CONDITION, with a value of -2, indicates that the current node is waiting for condition, that is, in the condition queue;
  4. PROPAGATE, with a value of -3, indicates that subsequent acquireShared can be executed in the current scenario;
  5. A value of 0 indicates that the current node is in the sync queue waiting to acquire a lock.

Node Additional Information:

Node prev Precursor Node
Node next Successor Node
Node nextWaiter Store successor nodes in condition queue
Thread thread Current thread on queue entry

exclusive lock

Locks can only be held by one thread lock at a point in time. AQS implements ReentrantLock, which is divided into fair locks and unfair locks.

  • fair lock

    Ensures the order in which threads acquire locks under multiple threads, while first-come threads acquire locks first

  • Unfair Lock

    When locking, try to acquire locks without regard to queue waiting, instead of automatically waiting at the end of the queue

Shared Lock

Locks can be acquired simultaneously by multiple threads at a point in time, AQS implements CountDownLatch, ReadWriteLock

1. AQS implements ReentrantLock unfair lock

ReentrantLock Unfair Lock Acquisition Lock

1,lock()

final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

2,acquire()

Gets the new state by executing the compareAndSetState method

public final void acquire(int arg) {
//If tryAcquire succeeds, acquire ends;
        if (!tryAcquire(arg) &&
            //The AcquireQueued method blocks until a lock is acquired
            //addWaiter adds the current thread to the end of the queue
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            //Interrupt the current thread if tryAcquire fails and acquiredQueued succeeds
            selfInterrupt();
    }
  1. Attempting to acquire a lock;
  2. If not, construct the current thread as Node and join the queue;
    The addWaiter method queues nodes, each thread being a node Node, forming a two-way queue, similar to a CLH queue.
  3. Try fetching again, if you don't get the one that pulls the current thread off the thread scheduler and goes into a wait state.

3,tryAcquire()

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
    //Get Status
            int c = getState();
    //If the lock is not occupied by any thread, it can be acquired by the current thread
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
    //If fetched to see if it is being checked by the current thread
            else if (current == getExclusiveOwnerThread()) {
                //Get it again if it is the current thread, count + 1
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

The Synchronizer-provided method of state operation is used in the tryAcquire method, and CAS principle ensures that only one thread can successfully modify the state, while threads that have not been successfully modified are queued.

4,acquireQueued()

The AcquireQueued method blocks until a lock is acquired

//node is null, exclusive blocking wait
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //p is the previous node of the current node
                final Node p = node.predecessor();
                //Set the current line node as the head node if p is the head node and succeeds
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //interrputr is true if the thread needs to be blocked
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
  1. Gets the precursor node of the current node;
  2. The current drive node is the head node and can acquire the state, which means that the current node holds the lock.
  3. Otherwise, enter the waiting state.

5,shouldParkAfterFailedAcquire()

Determine if a thread needs to be blocked

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        //Threads need to run
        if (ws == Node.SIGNAL)
            return true;
       //Ws>0 Thread in CANCEL State
        if (ws > 0) {
        //Clear these threads from the queue
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
         //Set Waiting to Running State
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
  1. First determine if the thread is running
  2. Clear them all from the queue if they are in CANCEL state
  3. Set the current node's next node waiting state to ready to run

6,parkAndCheckInterrupt()

// The park method leaves other threads waiting
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
  1. Call blocking thread local method park
  2. Returns whether the thread is blocked

ReentrantLock Unfair Lock Release Lock

1,unlock()

Call release to release a lock

public void unlock() {
    //Release a lock
        sync.release(1);
    }

2,release()

Release lock

public final boolean release(int arg) {
//Call tryRelease to attempt to release a lock 
        if (tryRelease(arg)) {
            Node h = head;
            //After successful release
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
  1. Calling the tryRelease method to attempt to release the lock failed to return false
  2. Release success determines that the header node is not null and the state is not waiting to acquire the lock state
  3. Wake up the thread if the condition is met and return true

3,tryRelease()

Attempt to release the current lock

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;
        }
  1. Release lock must be the current thread, if not the current thread threw an exception
  2. If the thread that acquires the lock is the current thread, it determines whether the state after release is 0, that is, the CANCEL state before release. If so, it sets the exclusive mode thread to null and sets the state to 0, returning true.

4,unparkSuccessor()

Wake up the thread after the lock is released to compete for CPU resources together

private void unparkSuccessor(Node node) {
        //Get the current node state
        int ws = node.waitStatus;
        //Set state to wait for lock to be acquired
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        //If there are no next nodes or if the next node state is CANCEL, clear them
        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;
        }
        //Otherwise, call the unpark method in LockSupport to wake up the next node
        if (s != null)
            LockSupport.unpark(s.thread);
    }
  1. Get the current node state
  2. Judges that if the state is <0, that is, not waiting to acquire the lock state, the state is set to waiting to acquire the lock state
  3. If there are no next nodes or if the next node state is CANCEL, clear them
  4. When the next node of the current node is not null, call the unpark method in LockSupport to wake up the next node

5,unpark()

Call UNSAFE's local method unpark to wake up a thread

public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

2. AQS Implements ReentrantLock Fair Lock

The biggest difference between AQS implementing ReentrantLock fair locks and unfair locks is the following code:

The source code is as follows:

public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

=== Determine if the Current Thread is at the top of the CLH queue to achieve fairness==.

3. Custom lock implemented by AQS

public class MyAQSLock implements Lock {
    private final Sync sync;
    public MyAQSLock() {
        sync = new Sync();
    }
    @Override
    public void lock() {
        sync.acquire(1);
    }
    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1,unit.toNanos(time));
    }
    @Override
    public void unlock() {
        sync.release(1);
    }
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
    /**
     * Build lock, unlock implementations into internal classes using AQS
     */
    private class Sync extends AbstractQueuedSynchronizer{
        Condition newCondition(){
            return new ConditionObject();
        }
        @Override
        protected boolean tryAcquire(int arg) {
            //The first thread came in to get the lock
            int state = getState();
            //Used for reentrant lock judgment
            Thread current = Thread.currentThread();
            if(state==0){
                if(compareAndSetState(0,arg)){
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
            }
            //Re-enter Lock Judges that the current thread is the same as the exclusive lock thread and retrieves it again
            else if(current==getExclusiveOwnerThread()){
                int next = state+arg;
                if(next<0){
                    throw new RuntimeException();
                }
                setState(next);
                return true;
            }
            return false;
        }
        /**
         * Reentrant release lock
         * @param arg
         * @return
         */
        @Override
        protected boolean tryRelease(int arg) {
            if(Thread.currentThread()!=getExclusiveOwnerThread()){
                throw new RuntimeException();
            }
            int state = getState()-arg;
            if(state==0){
                setExclusiveOwnerThread(null);
                setState(0);
                return true;
            }
            setState(0);
            return false;
        }

    }
}

Test (Re-Lockable)

public class TestAQSLock2 {
    MyAQSLock myLock = new MyAQSLock();
    private int value;
    private int value2;
    public int a(){
        myLock.lock();
        try {
            b();
            return value++;
        }finally {
            myLock.unlock();
        }
    }
    public void b(){
        myLock.lock();
        try {
            System.out.println(++value2);
        }finally {
            myLock.unlock();
        }
    }
    public static void main(String[] args) {
        TestAQSLock2 myLock = new TestAQSLock2();
        for(int i=0;i<50;i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + ":" + myLock.a());
            }).start();
        }
    }
}

Posted by runei on Wed, 13 Nov 2019 23:17:21 -0800