Understanding Java - AQS

Keywords: Java jvm JDK

java.util.concurrent provides many synchronizers, such as the commonly used ReentranLock, ReentrantReadWriteLock, Semaphore and CountDownLatch. They all rely on the processing provided by the AbstractQueuedSynchronizer class.

ReentranLock

ReentranLock is a reentrant Lock, similar to synchronized. The biggest difference is that synchronized is implemented at the JVM level, while Lock is implemented at the JDK level.

  1. usage method:
ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.unlock();
  1. Classes involved:
  2. Fair lock and unfair lock
      ReentranLock is divided into fair lock and unfair lock. The difference is whether the opportunity to get the lock is related to the queuing order.
      if the lock is held by another thread, the other thread applying for the lock will be suspended to wait and join the waiting queue. The thread that calls the lock function first is hung up and the waiting thread will be placed at the front end of the waiting queue. If the lock is released at this time, the waiting thread needs to be notified to try to acquire the lock again.
      fair locks let the first thread in the queue get the lock. Non fair locks attempt to acquire locks at lock time, so it may lead to later threads acquiring locks first, which is non fair.
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

AbstractQueuedSynchronizer

AQS is the abbreviation of AbstractQueuedSynchronizer. It provides a FIFO queue based infrastructure that can be used to build locks or other related synchronizations.

                     . The manipulation of state in a multithreaded environment must ensure atomicity. Therefore, for the processing of state, the following three methods provided by synchronizer are used to operate the state:

java.util.concurrent.locks.AbstractQueuedSynchronizer.getState()
java.util.concurrent.locks.AbstractQueuedSynchronizer.setState(int)
java.util.concurrent.locks.AbstractQueuedSynchronizer.compareAndSetState(int, int)
  1. Lock up
    The lock of ReentranLock, which directly calls the lock method of sync (the lock of FairSync or NonfairSync).
    // ReentranLock
    public void lock() {
        sync.lock();
    }
    // FairSync
    final void lock() {
        acquire(1);
    }
    // NonfairSync
    final void lock() {
	    if (compareAndSetState(0, 1))
	        setExclusiveOwnerThread(Thread.currentThread());
	    else
        	acquire(1);
   }

In the acquire method of AQS, tryAcquire attempts to acquire the lock first. If it obtains the lock, it will execute normally. If the lock is not available, execute addWaiter to create a node for the current thread, add it to the waiting queue, and then acquirequeueued attempts to acquire the lock. Still can not get the lock, call selfInterrupt for terminal processing.

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  1. Attempt to acquire lock
    tryAcquire is implemented in a specific synchronizer. Take FairSync as an example. The code is as follows:
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;
}

   getState gets the state variable in AQS. A value of 0 indicates that it has not been occupied by the thread. hasQueuedPredecessors determines whether there is a first coming thread waiting on the current blocking queue (which is not determined by UnfairSync). CAS optimistic lock attempts to change the exclusive variable. If it succeeds, it means that the current thread obtains the ownership of the variable, that is, the lock succeeds. setExclusiveOwnerThread sets this thread as the exclusive variable owner thread.
   if the thread has obtained the ownership of exclusive variables, then according to the reentry principle, add 1 to the state value to indicate multiple lock s.
                 .

The compareAndSetState function uses CAS operation to set the value of state, and the state value is set with the volatile modifier, so as to ensure that there is no multi-threaded problem in modifying the value of state.

  1. Join queue
    In the addWaiter method, compareAndSetTail is called to try to add the current thread to the queue. If it fails, join the queue when calling enq (more complex and time-consuming algorithm). Enq uses the for loop and keeps trying to join the queue.
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred; 
        if (compareAndSetTail(pred, node)){ 
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}
private Node enq(final Node node) {
    for (;;) { 
        Node t = tail;
        if (t == null) { //Initialization
            if (compareAndSetHead(new Node())) 
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
  1. Blocking queue
    Acquirequeueued executes a for loop until the lock is acquired.
    In the loop: determine whether the current node should get this variable (at the head of the team)? If it's the team leader, tryAcquire tries to get the exclusive variable again. If successful, return false, normal processing (i.e. no blocking). If the acquisition fails, call shouldParkAfterFailedAcquire to determine whether it should enter the blocking state. If the node before the current node has entered the blocking state, then it can be determined that the current node cannot obtain the lock. In order to prevent the CPU from executing the for loop and consuming CPU resources, the parkAndCheckInterrupt function is called to enter the blocking state
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); //If finished, set yourself to head
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //Call parkAndCheckInterrupt to block, and then return whether it is an interrupt state
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL) //The previous node is waiting for notification, so the current node can block
        return true;
    if (ws > 0) { //If the previous node is in cancel state, skip the previous node
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        //Set the status of the previous node to signal, and return false,
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); //Passing AQS objects in by themselves
    return Thread.interrupted();
}
  1. Blocking and interrupting
    AQS blocks the current process by calling LockSupport's park method.
public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);//Set blocking object to record who blocked the thread, and use it for thread monitoring and analysis tools to locate
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}
  1. Release lock
    The unlock operation calls the release method of AQS to release exclusive variables. If it succeeds, it depends on whether there are blocking threads waiting for locks, and if so, it calls unparkSuccessor to wake them up.
    Only when the value of state is 0, can the reenterable lock be released. So the value of the exclusive variable state represents whether the lock exists or not. When state=0, it means that the lock is not occupied. If not, it means that the current lock has been occupied.
    After the unpark method is called, the blocked thread will return to the running state, and the operation in infinite for loop in acquireQueued will be executed again, and the lock will be acquired again.
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;
}
private void unparkSuccessor(Node node) {
    .....
     //In general, the thread that needs to wake up is the next node of the head, but if its operation to acquire the lock is cancelled, or when the node is null
     //We will continue to traverse directly to find the first unreleased successor node
    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);
}

Overall concept map

Published 6 original articles, won praise 0, visited 129
Private letter follow

Posted by dcampbell18 on Wed, 04 Mar 2020 23:34:58 -0800