浅析AQS的同步队列的进队过程

前言

      AQS是AbstractQueuedSynchronizer,是用来构建锁或者其他同步组件的基础框架,其中在AQS中同步器依赖内部的同步队列(一个FIFO的队列)来完成对同步状态的管理,因此了解AQS的同步队列对了解AQS有很大的帮助。

 

基础知识

     同步队列的基础数据结构是节点(Node),其中Node的字段如下:

volatile int waitStatus;

volatile Node prev;

volatile Node next;

volatile Thread thread;

Node nextWaiter;

其中waitStatus的可能的状态值如下:

waitStatus的状态值
状态值 解释说明
CANCELLED 值为1,表示在同步队列中等待超时或者被中断,等待状态被取消
SINGAL 值是-1,表示后继线程处在等待状态中,当前线程释放或者取消,将会通知后继节点
CONDITION 值是-2,表示线程处在等待条件中,当前状态是被阻塞
PROPAGATE 值是-3,表示下一次共享状态会无条件被传播下去
INIT 值是0,表示初始状态

上面表格讲述了waitStatus的几种状态,另外的几个字段prev表示前继节点,next表示后续节点,thread表示当前等待的线程,nextWaiter表示是等待队列的后继节点,如果当前节点是共享的,那么这个字段表示节点类型和等待队列的后继节点共用同一个字段。

源码分析

         有了上述基础知识后,我们接下来就进行源码分析了,以下源码均基于jdk1.8。

         首先看进队方法:

private Node enq(final Node node) {
        for (;;) {
            Node t = 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;
                }
            }
        }
    }

      进入等待队列的话,一般在尾节点后加入新的节点,AQS有个私有变量tail表示当前同步队列的尾节点。进队首先是在一个死循环中,首先判断队尾元素是否为null,如果为null的话表示当前同步队列为空,所以把当前节点加入作为同步队列的头结点,此时头结点也是尾节点,然后会再次进入循环,这时进入了else逻辑,则用cas设置尾节点,如果当前尾元素不为null,则直接追加到尾节点后面。注意到此时加入节点前使用了compareAndSetHead这个方法,可以看下这个方法的详细实现。

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

这个设置了head的方法使用了乐观锁的思想,即在设置乐观锁之前首先保存当前变量的副本,在设置时比较当前变量和之前保存的副本的值是否一致,如果一致的话,表明当前没有线程更改该变量,可以对当前值进行修改,如果不一致的话,则表明当前值被别的线程修改,所以需要重新获取该值得最新值,重新进行修改。

  如果尾节点不为空的话则表示当前同步队列有线程在等待,所以需要将当前节点的前置节点指向尾节点,并把当前节点设置为新的尾节点,同时让之前的尾节点的后继节点指向新节点。注意此时同样适用了乐观锁的方法。

  总结一下,只有前继节点是头结点才能够去尝试获取到同步状态,这样也避免无谓的自旋。

   下面分析一下AQS的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;
    }

由于此时只有头结点有同步状态,所以释放同步状态是一个单线程操作,即只需要判断头结点是否为null以及等待状态是否为0,unparkSuccessor表示取消当前线程的独占状态并通知后继结点,这样就实现了独占状态的释放。

以上是独占状态的获取与释放的源码分析,下面分析共享状态。

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态,代码如下:

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


private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

   在acquiredShared方法中,AQS通过调用tryAcquireShared(int arg)方法尝试获取同步状态,如果返回值大于等于0,表示当前线程能正常获取到同步状态,则直接返回。 反之,则说明没有获取到同步状态,进入自旋状态。

    在自旋状态中,如果前继节点是头部节点时,则尝试获取同步状态,如果获取同步状态成功,则说明获取同步状态成功,然后把当前节点设置成同步节点,并将状态传播给后继节点。

     同样的,对于共享式获取,也需要释放状态,源码如下:

      

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

private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

该方法在释放同步状态后,必须通知后继节点,它和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程(多个线程同时获取到了同步状态)。

你可能感兴趣的:(浅析AQS的同步队列的进队过程)