Multithreading & JVM & lock related - in-depth study of java synchronizer AQS

introduce:

AQS (AbstractQueuedSynchronizer class) is a framework for building locks and synchronizers. It defines an int state variable internally to represent the synchronization state. The related locks in the LOCK package (commonly used ReentrantLock, ReadWriteLock) are It is built based on AQS. However, these locks do not directly inherit AQS, but define a Sync class to inherit AQS. So why is this? Because: locks are oriented to users, while synchronizers are oriented to thread control , then aggregating synchronizers in the implementation of the lock instead of directly inheriting AQS can well isolate the concerns of the two.


AQS completes the management of the synchronization state through a two-way FIFO synchronization queue. When a thread fails to acquire the lock, it is added to the end of the queue. Let me take a look at this queue

The red node is the head node, which can be regarded as the node holding the lock,


public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    static final class Node {...}
    private transient volatile Node head;
    private transient volatile Node tail;
    private volatile int state;//同步状态

It can be seen from the above that it sets the head and tail to volatile, and the modification of these two nodes will be seen by other threads. In fact, we mainly complete the enqueue and dequeue by modifying these two nodes. Next Let's learn Node together


static final class Node {
    //该等待同步的节点处于共享模式
    static final Node SHARED = new Node();
    //该等待同步的节点处于独占模式
    static final Node EXCLUSIVE = null;

    //等待状态,这个和state是不一样的:有1,0,-1,-2,-3五个值
    volatile int waitStatus;
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;

    volatile Node prev;//前驱节点
    volatile Node next;//后继节点
    volatile Thread thread;//等待锁的线程
    //和节点是否共享有关
    Node nextWaiter;
    //Returns true if node is waiting in shared mode
    final boolean isShared() {
            return nextWaiter == SHARED;
        }

The following explains the meaning of the five waitStatus:

  • CANCELLED(1): The thread of this node may be in canceled (obsolete) state due to timeout or interruption. Once in this state, the node state will always be CANCELLED (obsolete), so it should be removed from the queue.
  • SIGNAL(-1): When the current node is SIGNAL, the successor node will be suspended, so after the current node releases the lock or is canceled, it must be awakened (unparking) its successor node.
  • CONDITION(-2) The thread of this node is in the waiting condition state and will not be regarded as a node on the synchronization queue until it is awakened (signal), set its value to 0, and re-enter the blocking state.
  • 0: newly added node
When the lock is acquired, not only one thread can hold the lock (or called the synchronization state), so there is a difference between the exclusive mode and the shared mode at this time, that is, it is identified by nextWait in the Node node. For example, ReentrantLock is an exclusive lock, and only one thread can acquire the lock, while the read lock of WriteAndReadLock can be acquired by multiple threads at the same time, but its write lock can only be held by one thread. This time, we first introduce the acquisition and release of locks (or synchronization states) in exclusive mode. This class uses the template method design pattern: defining the skeleton of an algorithm in an operation, and deferring the implementation of some steps to subclasses.

1. Obtaining exclusive mode synchronization status


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

This method first tries to acquire the lock (the specific implementation of tryAcquire(arg) is defined in the subclass), if acquired, the execution is completed, otherwise the current node is added to the end of the waiting queue through the addWaiter(Node.EXCLUSIVE), arg) method , and set to exclusive mode,


private Node addWaiter(Node mode) {
        //把当前线程包装为node,设为独占模式
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        //如果tail不为空,把node插入末尾
        if (pred != null) {
            node.prev = pred;
            //此时可能有其他线程插入,所以重新判断tail
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //此时可能有其他线程插入,所以重新判断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;
                }
            }
        }
    }

If the tail node is empty, execute enq(node); try again, and finally insert the node. After inserting the node into the end of the queue, it does not immediately suspend the thread in the node, because in the process of inserting it, the previous thread It may have been executed, so it will perform the spin operation acquireQueued(node, arg) first, and try to let the thread re-acquire the lock! When the condition is met and the lock is acquired, it can exit from the spin process, otherwise continue.


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 lock is not acquired, it is judged whether it should be suspended, and this judgment must be determined by the waitStatus of its predecessor node:


private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
        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;
}

If the waitStatus of the predecessor node is:

  • SIGNAL, return true to indicate that the current thread should be suspended, suspend the thread, and wait to be woken up, and then perform interrupt detection after being woken up. If the current thread is found to be interrupted, throw InterruptedException and exit the loop.
  • >0, kick the predecessor node out of the queue, return false
  • <0, also returns false, but first set the precursor node waitStatus to SIGNAL, so that the current node will be suspended in the next judgment.
Finally, we summarize the process of acquiring an exclusive lock:

AQS的模板方法acquire通过调用子类自定义实现的tryAcquire获取同步状态失败后->将线程构造成Node节点(addWaiter)->将Node节点添加到同步队列对尾(addWaiter)->节点以自旋的方法获取同步状态(acquirQueued)。在节点自旋获取同步状态时,只有其前驱节点是头节点的时候才会尝试获取同步状态,如果该节点的前驱不是头节点或者该节点的前驱节点是头节点单获取同步状态失败,则判断当前线程需要阻塞,如果需要阻塞则需要被唤醒过后才返回。

2. 独占模式同步状态的释放

既然是释放,那肯定是持有锁的该线程执行释放操作,即head节点中的线程释放锁.

AQS中的release释放同步状态和acquire获取同步状态一样,都是模板方法,tryRelease释放的具体操作都有子类去实现,父类AQS只提供一个算法骨架。


public final boolean release(int arg) {
if (tryRelease(arg)) {
        Node h = head;
if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
return true;
    }
return false;
}
/**如果node的后继节点不为空且不是作废状态,则唤醒这个后继节点,否则从末尾开始寻找合适的节点,如果找到,则唤醒*/
private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 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);
    }

过程:首先调用子类的tryRelease()方法释放锁,然后唤醒后继节点,在唤醒的过程中,需要判断后继节点是否满足情况,如果后继节点不为且不是作废状态,则唤醒这个后继节点,否则从tail节点向前寻找合适的节点,如果找到,则唤醒.

综上,我们已经描述完了独占锁的获取和释放,至于共享锁的操作,有时间会再聊,请持续关注!


Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325652984&siteId=291194637