SynchronousQueue 源码分析 (基于Java 8)

1. SynchronousQueue 功能简介

SynchronousQueue 是 BlockingQueue 家族中的一个成员, 不同于其他的成员, 它具有以下特性:

1. 整个 queue 没有容量, 表现为, 你每次进行put值进去时, 必须等待相应的 consumer 拿走数据后才可以再次 put 数据
2. queue 对应 peek, contains, clear, isEmpty ... 等方法其实是无效的
3. 整个 queue 分为 公平(TransferQueue FIFO)与非公平模式(TransferStack LIFO 默认) 
4. 若使用 TransferQueue, 则队列中永远会存在一个 dummy node
2. SynchronousQueue 构造函数

/**
 * Creates a {@code SynchronousQueue} with nonfair access policy
 */
public SynchronousQueue() { this(false); }

/**
 * Creates a {@code KSynchronousQueue} with the specified fairness policy
 * @param fair
 */
public SynchronousQueue(boolean fair){
    // 通过 fair 值来决定内部用 使用 queue 还是 stack 存储线程节点  
    transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}

我们可以看到默认使用的 TransferStack 作为内部节点容器, 我们可以通过 fair 来决定公平与否

3. 公平模式 TransferQueue
/**
 *  这是一个非常典型的 queue , 它有如下的特点
 *  1. 整个队列有 head, tail 两个节点
 *  2. 队列初始化时会有个 dummy 节点
 *  3. 这个队列的头节点是个 dummy 节点/ 或 哨兵节点, 所以操作的总是队列中的第二个节点(AQS的设计中也是这也)
 */

/** 头节点 */
transient volatile QNode head;
/** 尾节点 */
transient volatile QNode tail;
/**
 * Reference to a cancelled node that might not yet have been
 * unlinked from queue because it was last inserted node
 * when it was cancelled
 */
/**
 * 对应 中断或超时的 前继节点,这个节点存在的意义是标记, 它的下个节点要删除
 * 何时使用:
 *      当你要删除 节点 node, 若节点 node 是队列的末尾, 则开始用这个节点,
 * 为什么呢?
 *      大家知道 删除一个节点 直接 A.CASNext(B, B.next) 就可以,但是当  节点 B 是整个队列中的末尾元素时,
 *      一个线程删除节点B, 一个线程在节点B之后插入节点 这样操作容易致使插入的节点丢失, 这个cleanMe很像
 *      ConcurrentSkipListMap 中的 删除添加的 marker 节点, 他们都是起着相同的作用
 */
transient volatile QNode cleanMe;

TransferQueue(){
    /**
     * 构造一个 dummy node, 而整个 queue 中永远会存在这样一个 dummy node
     * dummy node 的存在使得 代码中不存在复杂的 if 条件判断
     */
    QNode h = new QNode(null, false);
    head = h;
    tail = h;
}

/**
 * 推进 head 节点,将 老节点的 oldNode.next = this, help gc,
 * 这种和 ConcurrentLinkedQueue 中一样
 */
void advanceHead(QNode h, QNode nh){
    if(h == head && unsafe.compareAndSwapObject(this, headOffset, h, nh)){
        h.next = h; // forget old next help gc
    }
}

/** 更新新的 tail 节点 */
void advanceTail(QNode t, QNode nt){
    if(tail == t){
        unsafe.compareAndSwapObject(this, tailOffset, t, nt);
    }
}

/** CAS 设置 cleamMe 节点 */
boolean casCleanMe(QNode cmp, QNode val){
    return cleanMe == cmp && unsafe.compareAndSwapObject(this, cleanMeOffset, cmp, val);
}

从代码中我们知道, TransferQueue 是个 dual queue, 初始化时默认会个一个 dummy node;
而最特别的是 cleanMeNode, cleanMeNode是一个标记节点, cleanMeNode.next 节点是因中断或超时需要删除的节点,是在清除 队列最尾端节点时, 不直接删除这个节点, 而是间删除节点的前继节点标示为 cleanMe 节点, 为下次删除做准备, 功能和 ConcurrentSkipListMap 中的 marker 节点差不多, 都是防止在同一地点插入节点的同时因删除节点而造成节点的丢失, 不明白的可以看 ConcurrentSkipListMap.

3. 公平模式 TransferQueue transfer方法

这个方法的主逻辑:

1. 若队列为空 / 队列中的尾节点和自己的 类型相同, 则添加 node
   到队列中, 直到 timeout/interrupt/其他线程和这个线程匹配
   timeout/interrupt awaitFulfill方法返回的是 node 本身
   匹配成功的话, 要么返回 null (producer返回的), 或正真的传递值 (consumer 返回的)

2. 队列不为空, 且队列的 head.next 节点是当前节点匹配的节点,
   进行数据的传递匹配, 并且通过 advanceHead 方法帮助 先前 block 的节点 dequeue

直接看代码 transfer

   /**
 * Puts or takes an item
 * 主方法
 *
 * @param e  if non-null, the item to be handed to a consumer;
 *           if null, requests that transfer return an item
 *           offered by producer.
 * @param timed if this operation should timeout
 * @param nanos the timeout, in nanosecond
 * @return
 */
@Override
E transfer(E e, boolean timed, long nanos) {
    /**
     * Basic algorithm is to loop trying to take either of
     * two actions:
     *
     * 1. If queue apparently empty or holding same-mode nodes,
     *    try to add node to queue of waiters, wait to be
     *    fulfilled (or cancelled) and return matching item.
     *
     * 2. If queue apparently contains waiting items, and this
     *    call is of complementary mode, try to fulfill by CAS'ing
     *    item field of waiting node and dequeuing it, and then
     *    returning matching item.
     *
     * In each case, along the way, check for gurading against
     * seeing uninitialized head or tail value. This never
     * happens in current SynchronousQueue, but could if
     * callers held non-volatile/final ref to the
     * transferer. The check is here anyway because it places
     * null checks at top of loop, which is usually faster
     * than having them implicity interspersed
     *
     * 这个 producer / consumer 的主方法, 主要分为两种情况
     *
     * 1. 若队列为空 / 队列中的尾节点和自己的 类型相同, 则添加 node
     *      到队列中, 直到 timeout/interrupt/其他线程和这个线程匹配
     *      timeout/interrupt awaitFulfill方法返回的是 node 本身
     *      匹配成功的话, 要么返回 null (producer返回的), 或正真的传递值 (consumer 返回的)
     *
     * 2. 队列不为空, 且队列的 head.next 节点是当前节点匹配的节点,
     *      进行数据的传递匹配, 并且通过 advanceHead 方法帮助 先前 block 的节点 dequeue
     */
    QNode s = null; // constrcuted/reused as needed
    boolean isData = (e != null); // 1.判断 e != null 用于区分 producer 与 consumer

    for(;;){
        QNode t = tail;
        QNode h = head;
        if(t == null || h == null){         // 2. 数据未初始化, continue 重来
            continue;                       // spin
        }
        if(h == t || t.isData == isData){   // 3. 队列为空, 或队列尾节点和自己相同 (注意这里是和尾节点比价, 下面进行匹配时是和 head.next 进行比较)
            QNode tn = t.next;
            if(t != tail){                  // 4. tail 改变了, 重新再来
                continue;
            }
            if(tn != null){                 // 5. 其他线程添加了 tail.next, 所以帮助推进 tail
                advanceTail(t, tn);
                continue;
            }
            if(timed && nanos <= 0){        // 6. 调用的方法的 wait 类型的, 并且 超时了, 直接返回 null, 直接见 SynchronousQueue.poll() 方法,说明此 poll 的调用只有当前队列中正好有一个与之匹配的线程在等待被【匹配才有返回值
                return null;
            }
            if(s == null){
                s = new QNode(e, isData);  // 7. 构建节点 QNode
            }
            if(!t.casNext(null, s)){      // 8. 将 新建的节点加入到 队列中
                continue;
            }

            advanceTail(t, s);             // 9. 帮助推进 tail 节点
            Object x = awaitFulfill(s, e, timed, nanos); // 10. 调用awaitFulfill, 若节点是 head.next, 则进行一些自旋, 若不是的话, 直接 block, 知道有其他线程 与之匹配, 或它自己进行线程的中断
            if(x == s){                   // 11. 若 (x == s)节点s 对应额线程 wait 超时 或线程中断, 不然的话 x == null (s 是 producer) 或 是正真的传递值(s 是 consumer)
                clean(t, s);              // 12. 对接点 s 进行清除, 若 s 不是链表的最后一个节点, 则直接 CAS 进行 节点的删除, 若 s 是链表的最后一个节点, 则 要么清除以前的 cleamMe 节点(cleamMe != null), 然后将 s.prev 设置为 cleanMe 节点, 下次进行删除 或直接将 s.prev 设置为cleanMe
                return null;
            }

            if(!s.isOffList()){          // 13. 节点 s 没有 offlist
                advanceHead(t, s);       // 14. 推进head 节点, 下次就调用 s.next 节点进行匹配(这里调用的是 advanceHead, 因为代码能执行到这边说明s已经是 head.next 节点了)
                if(x != null){          // and forget fields
                    s.item = s;
                }
                s.waiter = null;       // 15. 释放线程 ref
            }

            return (x != null) ? (E)x :e;

        }else{                              // 16. 进行线程的匹配操作, 匹配操作是从 head.next 开始匹配 (注意 队列刚开始构建时 有个 dummy node, 而且 head 节点永远是个 dummy node 这个和 AQS 中一样的)
            QNode m = h.next;               // 17. 获取 head.next 准备开始匹配
            if(t != tail || m == null || h != head){
                continue;                  // 18. 不一致读取, 有其他线程改变了队列的结构inconsistent read
            }

            /** producer 和 consumer 匹配操作
             *  1. 获取 m的 item (注意这里的m是head的next节点
             *  2. 判断 isData 与x的模式是否匹配, 只有produce与consumer才能配成一对
             *  3. x == m 判断是否 节点m 是否已经进行取消了, 具体看(QNOde#tryCancel)
             *  4. m.casItem 将producer与consumer的数据进行交换 (这里存在并发时可能cas操作失败的情况)
             *  5. 若 cas操作成功则将h节点dequeue
             *
             *  疑惑: 为什么将h进行 dequeue, 而不是 m节点
             *  答案: 因为每次进行配对时, 都是将 h 是个 dummy node, 正真的数据节点 是 head.next
             */
            Object x = m.item;
            if(isData == (x != null) ||    // 19. 两者的模式是否匹配 (因为并发环境下 有可能其他的线程强走了匹配的节点)
                    x == m ||               // 20. m 节点 线程中断或者 wait 超时了
                    !m.casItem(x, e)        // 21. 进行 CAS 操作 更改等待线程的 item 值(等待的有可能是 concumer / producer)
                    ){
                advanceHead(h, m);          // 22.推进 head 节点 重试 (尤其 21 操作失败)
                continue;
            }

            advanceHead(h, m);             // 23. producer consumer 交换数据成功, 推进 head 节点
            LockSupport.unpark(m.waiter); // 24. 换线等待中的 m 节点, 而在 awaitFulfill 方法中 因为 item 改变了,  所以 x != e 成立, 返回
            return (x != null) ? (E)x : e; // 25. 操作到这里若是 producer, 则 x != null, 返回 x, 若是consumer, 则 x == null,.返回 producer(其实就是 节点m) 的 e
        }
    }

}

OK, 我们梳理一下一般性的流程:

1. 一开始整个queue为空, 线程直接封装成QNode, 通过 awaitFulfill 方法进入自旋等待状态, 除非超时或线程中断, 不然一直等待, 直到有线程与之匹配
2. 下个再来的线程若isData与尾节点一样, 则进行第一步, 不然进行数据转移(步骤 21), 然后 unpark 等待的线程
3. 等待的线程被唤醒, 从awaitFulfill方法返回, 最后将结果返回
4. 公平模式 TransferQueue awaitFulfill
/**
 * Spins/blocks until node s is fulfilled
 *
 * 主逻辑: 若节点是 head.next 则进行 spins 一会, 若不是, 则调用 LockSupport.park / parkNanos(), 直到其他的线程对其进行唤醒
 *
 * @param s the waiting node
 * @param e the comparsion value for checking match
 * @param timed true if timed wait
 * @param nanos timeout value
 * @return  matched item, or s of cancelled
 */
Object awaitFulfill(QNode s, E e, boolean timed, long nanos){

    final long deadline = timed ? System.nanoTime() + nanos : 0L;// 1. 计算 deadline 时间 (只有 timed 为true 时才有用)
    Thread w = Thread.currentThread();   // 2. 获取当前的线程
    int spins = ((head.next == s) ?        // 3. 若当前节点是 head.next 时才进行 spin, 不然的话不是浪费 CPU 吗, 对挖
            (timed ? maxTimeSpins : maxUntimedSpins) : 0);
    for(;;){                                        // loop 直到 成功
        if(w.isInterrupted()){                      // 4. 若线程中断, 直接将 item = this, 在 transfer 中会对返回值进行判断 (transfer中的 步骤 11)
            s.tryCancel(e);
        }
        Object x = s.item;
        if(x != e){                                 // 5. 在进行线程阻塞->唤醒, 线程中断, 等待超时, 这时 x != e,直接return 回去
            return x;
        }
        if(timed){
            nanos = deadline - System.nanoTime();
            if(nanos <= 0L){                        // 6. 等待超时, 改变 node 的item值, 进行 continue, 下一步就到  awaitFulfill的第 5 步 -> return
                s.tryCancel(e);
                continue;
            }
        }
        if(spins > 0){                             // 7. spin 一次一次减少
            --spins;
        }
        else if(s.waiter == null){
            s.waiter = w;
        }
        else if(!timed){                           // 8. 进行没有超时的 park
            LockSupport.park(this);
        }
        else if(nanos > spinForTimeoutThreshold){  // 9. 自旋次数过了, 直接 + timeout 方式 park
            LockSupport.parkNanos(this, nanos);
        }
    }
}

梳理逻辑:

1. 计算timeout时间(若 time = true)
2. 判断 当前节点是否是 head.next 节点(queue中有个dummy node 的存在, AQS 中也是这样), 若是的话就进行 spin 的赋值, 其他的节点没有这个需要, 浪费资源
3. 接下来就是自旋, 超过次数就进行阻塞, 直到有其他线程唤醒, 或线程中断(这里线程中断返回的是 Node 自己)
5. 公平模式 TransferQueue clean
/**
 * Gets rid of cancelled node s with original predecessor pred.
 * 对 中断的 或 等待超时的 节点进行清除操作
 */
void clean(QNode pred, QNode s) {
    s.waiter = null; // forget thread                                        // 1. 清除掉 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.
     *
     * 在程序运行中的任何时刻, 最后插入的节点不能被删除(这里的删除指 通过 cas 直接删除, 因为这样直接删除会有多删除其他节点的风险)
     * 当 节点 s 是最后一个节点时, 将 s.pred 保存为 cleamMe 节点, 下次再进行清除操作
     */
    while (pred.next == s) { // Return early if already unlinked           // 2. 判断 pred.next == s, 下面的 步骤2 可能导致 pred.next = next
        QNode h = head;
        QNode hn = h.next;   // Absorb cancelled first node as head
        if (hn != null && hn.isCancelled()) {                              // 3. hn  中断或者超时, 则推进 head 指针, 若这时 h 是 pred 则 loop 中的条件 "pred.next == s" 不满足, 退出 loop
            advanceHead(h, hn);
            continue;
        }
        QNode t = tail;      // Ensure consistent read for tail
        if (t == h)                                                        // 4. 队列为空, 说明其他的线程进行操作, 删除了 节点(注意这里永远会有个 dummy node)
            return;
        QNode tn = t.next;
        if (t != tail)                                                    // 5. 其他的线程改变了 tail, continue 重新来
            continue;
        if (tn != null) {
            advanceTail(t, tn);                                            // 6. 帮助推进 tail
            continue;
        }
        if (s != t) {        // If not tail, try to unsplice              // 7. 节点 s 不是尾节点, 则 直接 CAS 删除节点(在队列中间进行这种删除是没有风险的)
            QNode sn = s.next;
            if (sn == s || pred.casNext(s, sn))
                return;
        }

        QNode dp = cleanMe;                                             // 8. s 是队列的尾节点, 则 cleanMe 出场
        if (dp != null) {    // Try unlinking previous cancelled node
            QNode d = dp.next;                                          // 9. cleanMe 不为 null, 进行删除删一次的 s节点, 也就是这里的节点d
            QNode dn;
            if (d == null ||               // d is gone or              // 10. 这里有几个特殊情况 1. 原来的s节点()也就是这里的节点d已经删除; 2. 原来的节点 cleanMe 已经通过 advanceHead 进行删除; 3 原来的节点 s已经删除 (所以 !d.siCancelled), 存在这三种情况, 直接将 cleanMe 清除
                    d == dp ||                 // d is off list or
                    !d.isCancelled() ||        // d not cancelled or
                    (d != t &&                 // d not tail and        // 11. d 不是tail节点, 且dn没有offlist, 直接通过 cas 删除 上次的节点 s (也就是这里的节点d); 其实就是根据 cleanMe 来清除队列中间的节点
                            (dn = d.next) != null &&  //   has successor
                            dn != d &&                //   that is on list
                            dp.casNext(d, dn)))       // d unspliced
                casCleanMe(dp, null);                                  // 12. 清除 cleanMe 节点, 这里的 dp == pred 若成立, 说明清除节点s, 成功, 直接 return, 不然的话要再次 loop, 接着到 步骤 13, 设置这次的 cleanMe 然后再返回
            if (dp == pred)
                return;      // s is already saved node
        } else if (casCleanMe(null, pred))                          // 原来的 cleanMe 是 null, 则将 pred 标记为 cleamMe 为下次 清除 s 节点做标识
            return;          // Postpone cleaning s
    }
}

clean 方法是 整个代码分析过程中的难点:

1. 难在并发的情况比较多
2. cleanMe 节点存在的意义

调用这个方法都是由 节点线程中断或等待超时时调用的, 清除时分两种情况讨论:

1. 删除的节点不是queue尾节点, 这时 直接 pred.casNext(s, s.next) 方式来进行删除(和ConcurrentLikedQueue中差不多)
2. 删除的节点是队尾节点
  1) 此时 cleanMe == null, 则 前继节点pred标记为 cleanMe, 为下次删除做准备
  2) 此时 cleanMe != null, 先删除上次需要删除的节点, 然后将 cleanMe至null, 让后再将 pred 赋值给 cleanMe
这时我们想起了 ConcurrentSkipListMap 中的 marker 节点, 对, marker 和 cleanMe 都是起着防止并发环境中多删除节点的功能
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容