Java Thread&Concurrency(3): 深入理解SynchronousQueue实现原理

背景:

一个BlockingQueue的是一个这样的队列,每个插入操作都必须等待另一个删除操作,反过来也一样。一个同步队列没有内部容量这个概念。你不能使用peek操作,因为一个元素仅在你试着删除它的时候才能够被取得。你不能插入一个元素(任何方法),直到另一个线程试着删除它。你不能迭代它们,因为没有东西可以被迭代。queue的头元素head是第一个进入队列中的元素,并且插入线程正在为它等待。如果队列中没有像以上的元素存在,那么调用poll的方法会返回null。对于Collection的其他方法(比如contains),SynchronousQueue表现得像一个空的集合。它不允许null入队。

这个队列类似于CSP和Ada中使用的会合信道。它们适合于切换的设计,比如一个线程中的对象必须同步等待另一个线程中运行的对象从而传递一些信息/事件/任务。

这个类支持可选的公平策略从而制订生产者和等待者的等待顺序。默认情况下,这个顺序是没有保证的,使用true可以确保队列是FIFO的。

这个类以及它的迭代器实现了某些Collection和Iterator中的方法。


算法:

这个算法实现了双重栈和双重队列算法。

(LIfo)栈用作非公平模式,(Fifo)队列用于公平模式。这两者的性能相似。Fifo经常能在竞争情况下提供更高的吞吐量,但是Lifo能够在一般应用中维持更高的线程局部性。

双重队列(以及相似的栈)在任何时刻都是持有“数据”--item通过put操作提供或者“请求”--slo通过take操作提供,或者为空。一个调用试着“满足”(一个请求的调用得到数据或者一个数据的调用匹配了请求)结果是出队了一个模式互补的节点。最有趣的的地方在于任何操作都能够明确当前队列处于哪种模式,以及表现得好像不需要锁。

队列和栈都扩展了抽象的Transferer接口,它定义了唯一一个transfer方法用于put或者take。这些方法统一在一个方法下是因为在双重数据结构中,put和take方法是对称的两种方法,所以几乎所有代码可以被组合。结果transfer方法是比较长的,但是这样相对于把他们分成几乎重复的几部分代码还是更好的。

这个队列和栈共享了很多相似的概念但是很少的具体细节。为了简单性,他们保持不同从而在后续可以分开演化。

这个同步队列的算法不同于以前的算法,包括消除的机制。主要的不同在于:

  • 其他的算法使用位-掩码的方式,但是现在的节点直接使用了模式位,从而导致了其他的不同。
  • 同步队列必须等待直到被其他线程来“满足”。
  • 提供了超时和中断的支持,包括从链表中清理节点/线程从而避免垃圾数据的持有和内存消耗。
阻塞操作主要通过LockSupport的park/unpark方法来实现,而那些很可能被下一个调用满足的节点会首先自旋一定次数(仅在多核处理器上)。在非常繁忙的同步队列中,自旋能够显著地提升吞吐量。但是在一般情况下,这个自旋次数也是足够小从而不那么明显。

清理操作在队列和栈中的方式是不同的。队列中,我们几乎使用O(1)的时间来清除被取消的节点。但是如果它正被作为tail元素,那么就必须等待后续的清除操作来清理它。栈中,我们需要潜在的O(n)时间来遍历,从而确认我们能够清除这个节点,但是这个操作可以和其他线程共同作用于这个栈。

当垃圾回收关注于大部分节点的回收问题时,复杂的非阻塞算法会试着“忘记”数据、其他节点的引用,以及线程会持有这些数据在一段长的阻塞时间内。在这种情况下给一个节点中的值设置null将会违背主要的算法,我们会代替使用将引用指向节点自身的方式。这个情况不怎么会在栈中发生(因为阻塞线程不会挂在旧的头节点上),但是队列中的节点引用必须积极地忘记,从而避免已经结束的节点的可达性。

实现:
实现的重点在于transfer这个方法:
  • 不能放入null元素,否则抛出NullPointerException异常。
  • 不限时版本(timed==false),返回非null为成功插入,返回null抛出InterruptedException异常。
  • 限时版本(timed==true),返回非null为成功插入,返回null的确情况下,若处于中断状态则抛出InterruptedException异常,否则表明超时。
Transferer接口分为queue和stack两个版本, 我们先分析queue版本(源码):

queue

        /**
         * Puts or takes an item.
         */
        @SuppressWarnings("unchecked")
        E transfer(E e, boolean timed, long nanos) {
            QNode s = null; // constructed/reused as needed
            boolean isData = (e != null);

            for (;;) {
                QNode t = tail;
                QNode h = head;
                if (t == null || h == null)         // saw uninitialized value
                    continue;                       // spin

                if (h == t || t.isData == isData) { // empty or same-mode
                    QNode tn = t.next;
                    if (t != tail)                  // inconsistent read
                        continue;
                    if (tn != null) {               // lagging tail
                        advanceTail(t, tn);
                        continue;
                    }
                    if (timed && nanos <= 0)        // can't wait
                        return null;
                    if (s == null)
                        s = new QNode(e, isData);
                    if (!t.casNext(null, s))        // failed to link in
                        continue;

                    advanceTail(t, s);              // swing tail and wait
                    Object x = awaitFulfill(s, e, timed, nanos);
                    if (x == s) {                   // wait was cancelled
                        clean(t, s);
                        return null;
                    }

                    if (!s.isOffList()) {           // not already unlinked
                        advanceHead(t, s);          // unlink if head
                        if (x != null)              // and forget fields
                            s.item = s;
                        s.waiter = null;
                    }
                    return (x != null) ? (E)x : e;

                } else {                            // complementary-mode
                    QNode m = h.next;               // node to fulfill
                    if (t != tail || m == null || h != head)
                        continue;                   // inconsistent read

                    Object x = m.item;
                    if (isData == (x != null) ||    // m already fulfilled
                        x == m ||                   // m cancelled
                        !m.casItem(x, e)) {         // lost CAS
                        advanceHead(h, m);          // dequeue and retry
                        continue;
                    }

                    advanceHead(h, m);              // successfully fulfilled
                    LockSupport.unpark(m.waiter);
                    return (x != null) ? (E)x : e;
                }
            }
        }


这里的基本算法是循环试着执行下列行动之一:

  • 如果队列是空的或者拥有和当前调用同样的模式,试着在队列中增加等待节点,等待这个节点被满足(或者取消),然后返回相匹配的数据。
  • 如果队列不为空以及队列拥有和当前调用相补的模式,试着去通过CAS操作来改变等待节点的item域,弹出队列,以及返回相匹配的数据。
在每一种情况中,都会检查并且帮助递增head和tail域,在其他的线程相对停滞和缓慢的情况下。
一开始对于null的检查是为了避免看到没有初始化的head和tail值。在当前的SynchronousQueue中这种情况不会发生,除非调用者使用的是非volatile/final的Transferer。
这个检查存在的原因是,放在循环的顶部快于将它在循环中穿插地放置。

详情如下:
  • 取得当前调用的模式isData,进入循环,首先排除head和tail都为null的情况。
  • 当队列为空(head=tail)或者队尾节点的模式和当前调用模式相同的情况下:假如tail元素已经改变则更新重来,假如当前是限时版本并且时间参数不大于0则返回null,创建新节点并且以CAS方式入队,更新tail节点,然后等待相补的调用的来临(awaitFulfill),假如返回的数据是节点本身(说明是中断或者超市)则做清理,并返回null,否则假如节点还在队列中则更新head元素并且修改当前节点的item和waiter域,然后返回获得的x和e中非空的元素(此处必定是1空1非空)。
  • 否则,队列中有相补模式的节点,则试着取得队首元素即head.next,尝试满足他(fulfill),假如失败则重试,成功之后会递增head的值,然后唤醒(unpark)等待线程以及返回相匹配的数据。


接着我们来看阻塞操作awaitFulfill:

        /**
         * Spins/blocks until node s is fulfilled.
         *
         * @param s the waiting node
         * @param e the comparison value for checking match
         * @param timed true if timed wait
         * @param nanos timeout value
         * @return matched item, or s if cancelled
        */
        Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
            /* Same idea as TransferStack.awaitFulfill */
            final long deadline = timed ? System.nanoTime() + nanos : 0L;
            Thread w = Thread.currentThread();
            int spins = ((head.next == s) ?
                         (timed ? maxTimedSpins : maxUntimedSpins) : 0);
            for (;;) {
                if (w.isInterrupted())
                    s.tryCancel(e);
                Object x = s.item;
                if (x != e)
                    return x;
                if (timed) {
                    nanos = deadline - System.nanoTime();
                    if (nanos <= 0L) {
                        s.tryCancel(e);
                        continue;
                    }
                }
                if (spins > 0)
                    --spins;
                else if (s.waiter == null)
                    s.waiter = w;
                else if (!timed)
                    LockSupport.park(this);
                else if (nanos > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanos);
            }
        }

这里的s是根据调用构造的节点,e为传递的数据(null是请求,否则数据),timed为限时标示,nanos时间量(纳秒):

  • 首先我们根据timed取得结束时间,假如非限时则为0,然后取得线程对象以及自旋次数:队首元素&非限时(maxUntimedSpins最大);队首元素&限时(maxTimedSpins次之);否则为0。
  • 进入循环:假如中断则尝试取消节点,假如item变化则返回数据,假如限时版本并且超时则尝试取消并重试或者更新nanos,递减自旋次数直到0----->设置节点中的线程对象----->调用非限时挂起版本或者根据阈值判断是否调用限时挂起版本。
注意:以上循环在有相补的调用发生时总是会返回对应的数据,在被中断或者超时处理成功情况下会返回当前节点本身。


我们最后看transfer方法中的清理操作:

        void clean(QNode pred, QNode s) {
            s.waiter = null; // forget thread
            /*
             * At any given time, exactly one node on list cannot be
             * deleted -- the last inserted node. To accommodate this,
             * if we cannot delete s, we save its predecessor as
             * "cleanMe", deleting the previously saved version
             * first. At least one of node s or the node previously
             * saved can always be deleted, so this always terminates.
             */
            while (pred.next == s) { // Return early if already unlinked
                QNode h = head;
                QNode hn = h.next;   // Absorb cancelled first node as head
                if (hn != null && hn.isCancelled()) {
                    advanceHead(h, hn);
                    continue;
                }
                QNode t = tail;      // Ensure consistent read for tail
                if (t == h)
                    return;
                QNode tn = t.next;
                if (t != tail)
                    continue;
                if (tn != null) {
                    advanceTail(t, tn);
                    continue;
                }
                if (s != t) {        // If not tail, try to unsplice
                    QNode sn = s.next;
                    if (sn == s || pred.casNext(s, sn))
                        return;
                }
                QNode dp = cleanMe;
                if (dp != null) {    // Try unlinking previous cancelled node
                    QNode d = dp.next;
                    QNode dn;
                    if (d == null ||               // d is gone or
                        d == dp ||                 // d is off list or
                        !d.isCancelled() ||        // d not cancelled or
                        (d != t &&                 // d not tail and
                         (dn = d.next) != null &&  //   has successor
                         dn != d &&                //   that is on list
                         dp.casNext(d, dn)))       // d unspliced
                        casCleanMe(dp, null);
                    if (dp == pred)
                        return;      // s is already saved node
                } else if (casCleanMe(null, pred))
                    return;          // Postpone cleaning s
            }
        }

方法中的pred为s入队时的前驱,这个方法中注释里有如下说明:

在任一时刻,只有一个链表中的节点不能被删除---最后一个插入的节点。为了迁就这个原则,如果我们无法删除s,那么我们就保存它的前驱节点为“cleanMe”,首先删除之前保存的版本。所以至少s或者之前保存的节点能够被删除,所以最后总是能够被删除!

详情如下:

  • 首先既然s已经被取消,则设置它的等待者waiter为null,进入循环(条件:pred.next为s推断出s不为head,s为head的话不需要删除了)。
  • 取得head和tail,探测他们是否停滞,过于停滞时更新他们的值。
  • 当s节点不为队尾节点时候(尝试更新前驱pred,或者当s处于OffList时返回)。
  • s节点为尾元素:假如cleanMe不为空,则说明有之前并未删除的尾节点,那么则尝试删除之前的cleanMe之后的节点,否则尝试设置当前的pred为cleanMe,等待下一次的删除,然后返回。
这里的难点在于: 当我们取得cleanMe时,如果不为空,并不了解是否有其他线程进过同样的操作,所以我们首先要判断d=dp.nex不为空(若为空则修改cleanMe然后返回)、d != dp(否则说明dp已经offList)、d.isCancelled(否则说明之前的节点已经被删除、以及d不为尾节点&&dn=d.next不为空&&dn != d(否则d已经offList),以上情况下才可以修改dp的next为dn从而删除了之前存储的待删除尾节点,并且修改cleanMe的值从dp变为null。假如cleanMe为空,则可以尝试设置自己的pred节点,在竞争失败的情况下可以重试是可以取得进展的,因为任何时候只有一个节点能够作为tail。
所以我们会尝试清理之前被删除的尾节点,以及尝试设置自己的前驱为cleanMe节点。

if (dp == pred)
这一行在我看来是没必要的,因为永远不可能出现。


我们接着分析stack:

stack

        /**
         * Puts or takes an item.
         */
        @SuppressWarnings("unchecked")
        E transfer(E e, boolean timed, long nanos) {


            SNode s = null; // constructed/reused as needed
            int mode = (e == null) ? REQUEST : DATA;

            for (;;) {
                SNode h = head;
                if (h == null || h.mode == mode) {  // empty or same-mode
                    if (timed && nanos <= 0) {      // can't wait
                        if (h != null && h.isCancelled())
                            casHead(h, h.next);     // pop cancelled node
                        else
                            return null;
                    } else if (casHead(h, s = snode(s, e, h, mode))) {
                        SNode m = awaitFulfill(s, timed, nanos);
                        if (m == s) {               // wait was cancelled
                            clean(s);
                            return null;
                        }
                        if ((h = head) != null && h.next == s)
                            casHead(h, s.next);     // help s's fulfiller
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    }
                } else if (!isFulfilling(h.mode)) { // try to fulfill
                    if (h.isCancelled())            // already cancelled
                        casHead(h, h.next);         // pop and retry
                    else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                        for (;;) { // loop until matched or waiters disappear
                            SNode m = s.next;       // m is s's match
                            if (m == null) {        // all waiters are gone
                                casHead(s, null);   // pop fulfill node
                                s = null;           // use new node next time
                                break;              // restart main loop
                            }
                            SNode mn = m.next;
                            if (m.tryMatch(s)) {
                                casHead(s, mn);     // pop both s and m
                                return (E) ((mode == REQUEST) ? m.item : s.item);
                            } else                  // lost match
                                s.casNext(m, mn);   // help unlink
                        }
                    }
                } else {                            // help a fulfiller
                    SNode m = h.next;               // m is h's match
                    if (m == null)                  // waiter is gone
                        casHead(h, null);           // pop fulfilling node
                    else {
                        SNode mn = m.next;
                        if (m.tryMatch(h))          // help match
                            casHead(h, mn);         // pop both h and m
                        else                        // lost match
                            h.casNext(m, mn);       // help unlink
                    }
                }
            }
        }


这里的Node实际上拥有三种状态:REQUEST/DATA/FULFILLING,基本算法是循环试着执行下列3种行动之一:

  • 如果栈为空或者包含相同模式的节点,试着入栈然后等待匹配,返回匹配数据,在取消时返回null。
  • 如果包含相补的节点,试着入栈一个fulfill模式的节点,匹配相对应的等待节点,弹出这两个节点,返回匹配数据。这里的匹配或者取消链接操作可能没有被实际执行,因为第三种行动:
  • 如果栈顶元素为fulfill模式的节点,尝试帮助它执行match以及pop操作,然后再重试。这里的代码和fulfill的行为几乎相同的,只不过不返回数据。
详情如下:
  • 取得当前调用的模式mode,REQUEST或则DATA,然后进入循环。
  • 取得栈顶元素h,当队列为空或者栈顶元素模式与当前模式相同时:首先排除限时调用和nanos不大于0的情况,然后返回null。否则链接到h,并尝试入栈,成功之后通过awaitFulfill等待相补调用的来临,然后根据返回SNode节点是否为s本身来判断是否需要清理操作,假如获取了数据那么便协助更新head,以及根据当前模式返回数据。
  • 当前栈顶元素与当前调用模式不同,那么假如当前栈顶元素模式不为fulfill时(通过 (mode & FULFILLING) != 0来判断),进入循环,s为当前栈顶元素,m为下一个待匹配元素(必定不为null),尝试取得mn(m.next),试着使用m.tryMatch(s),来完成m和s的匹配,并且传递s到了m.match(注意,这一步可能也会由另一种情况完成),成功之后改变head值,以及返回当前数据或者匹配数据。假如失败(意味着最后探测到的match不为s,唯一的场景为等待线程中断或者超时),则重新设置s的next值为mn,然后重试。
  • 当前栈顶元素的模式为fulfill时,尝试取得栈顶元素h和next节点m,然后试着匹配m和h的值,这里的作用几乎和上一种情况中类似,只不过不返回数据,以及只协助一次。

接着我们来看阻塞操作awaitFulfill:

        /**
         * Spins/blocks until node s is matched by a fulfill operation.
         *
         * @param s the waiting node
         * @param timed true if timed wait
         * @param nanos timeout value
         * @return matched node, or s if cancelled
         */
        SNode awaitFulfill(SNode s, boolean timed, long nanos) {
            final long deadline = timed ? System.nanoTime() + nanos : 0L;
            Thread w = Thread.currentThread();
            int spins = (shouldSpin(s) ?
                         (timed ? maxTimedSpins : maxUntimedSpins) : 0);
            for (;;) {
                if (w.isInterrupted())
                    s.tryCancel();
                SNode m = s.match;
                if (m != null)
                    return m;
                if (timed) {
                    nanos = deadline - System.nanoTime();
                    if (nanos <= 0L) {
                        s.tryCancel();
                        continue;
                    }
                }
                if (spins > 0)
                    spins = shouldSpin(s) ? (spins-1) : 0;
                else if (s.waiter == null)
                    s.waiter = w; // establish waiter so can park next iter
                else if (!timed)
                    LockSupport.park(this);
                else if (nanos > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanos);
            }
        }

这个操作事实上和队列版本中的类似,首先来解释下注释中的内容:当一个节点/线程试着去阻塞,它会在设置waiter域之后至少检查一次状态,然后才会调用parking(阻塞),这样子可以通过waiter从而和它的满足者协作从而确保不会丢失信号。如果当前调用的几点位于栈顶,那么在park之前会首先尝试自旋,这样可以在生产者和消费者非常接近时避免阻塞。但是这个只在多核处理器下才会有用。从代码中的检查情况可以看出,在优先级上,中断状态--->正式返回---->超时。(所以最后一个检查是用来探测超时的)除了非限时的同步队列。{poll/offer}方法不会检查中断以及等待太久,所以对于中断和超时的判断被放置于transfer方法中,这样要好于调用awaitFulfill。详情如下(与队列版本中类似):取得当前的结束时间,当前线程,以及自旋次数。然后进入循环。首先判断是否中断,判断限时版本下的时间流逝,判断自旋,以及根据当前节点s所处的位置来设置自旋次数。设置线程对象(用于唤醒)。最后根据是否限时来阻塞当前线程,限时版本下会根据阈值来判断是否需要阻塞。最后我们来看处理中断和超时情况下的清理操作clean:

 void clean(SNode s) {
            s.item = null;   // forget item
            s.waiter = null; // forget thread

            /*
             * At worst we may need to traverse entire stack to unlink
             * s. If there are multiple concurrent calls to clean, we
             * might not see s if another thread has already removed
             * it. But we can stop when we see any node known to
             * follow s. We use s.next unless it too is cancelled, in
             * which case we try the node one past. We don't check any
             * further because we don't want to doubly traverse just to
             * find sentinel.
             */

            SNode past = s.next;
            if (past != null && past.isCancelled())
                past = past.next;

            // Absorb cancelled nodes at head
            SNode p;
            while ((p = head) != null && p != past && p.isCancelled())
                casHead(p, p.next);

            // Unsplice embedded nodes
            while (p != null && p != past) {
                SNode n = p.next;
                if (n != null && n.isCancelled())
                    p.casNext(n, n.next);
                else
                    p = n;
            }
        }

这里的s为被取消了的节点,这里的注释有如下说明:

最坏情况下我们需要遍历整个栈才能取消s的链接。如果有其他的取消操作同时在进行,我们可能看不到s,因为它已经被其他的线程删除了。但是我们可以观察跟随s之后的节点,如果这个节点也是取消状态,那么我们会使用下一个节点,我们不会再检查,因为不想要遍历两遍仅仅是为了找到哨兵节点。

详情如下:
  • 设置s节点的item和waiter都为null,因为已经不需要了,并且它的状态可以由match为this来判断。
  • 取得s的下一个节点past,假如past也是取消的,那么再取下一节点。
  • 从头p=head开始,逐步断开past之前的那些被取消的节点。
  • 再从p开始删除嵌在栈中的节点,知道栈为空或者找到哨兵节点(past)。


  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值