SynchronousQueue之TransferStack源码分析

665 阅读5分钟

简介

SynchronousQueue是一个同步阻塞队列,它的每个插入操作或者获取操作都要等待其它线程相应的获取或者插入,内部提供两种策略,公平模式和非公平模式,这一小节先分析默认策略,非公平模式。

非公平模式采用栈的模式,FILO,它内部有三种状态,分别是REQUEST、DATA、FULFILLING,其中REQUEST代表是消费者,DATA代表是生产者,FULFILLING代表正在匹配中。

当有相同模式情况下进行入栈操作,相同操作指的是REQUEST和DATA两种类型中任意一种进行操作时,模式相同则进行入栈操作,如下图所示:

同REQUEST进行获取数据时的入栈情况:

2.png

同样的put的操作,进行数据操作时为DATA类型的操作,此时队列情况为:

3.png

不同模式下又是如何进行操作的?当有不同模式进来的时候,他不是将当前的模式压入栈顶,而是将FullFill模式和当前模式进行按位或之后压入栈顶,也就是压入一个进行FullFill请求的模式进入栈顶,请求配对操作,如下图所示:

4.png

通过上图可见,本来栈中有一个DATA模式的数据等待消费者进行消费,这时候来了一个REQUEST模式的请求操作来进行消费数据,这时候并没有将REQUEST模式直接压入栈顶,而是将其转换为FULLFILLING模式,并且保留了原有的类型,这是进行FULLFILLING的请求,请求和栈顶下方元素进行匹配,当匹配成功后将栈顶和匹配元素同时进行出栈操作,详细请见下文分析:

TransferStack

主要属性

/** 代表当前节点是消费节点 */
static final int REQUEST    = 0;

/** 代表当前节点是生产节点 */
static final int DATA       = 1;

/** 当前节点正在匹配 有可能是消费匹配生产 有可能是生产匹配消费 */
static final int FULFILLING = 2;

/** 栈顶节点 */
volatile SNode head;

主要方法

方法名描述
isFulfilling判断节点是否是匹配节点
casHead原子更新head节点
snode创建node节点并指定next节点
transferput或者take都会调用此方法
awaitFulfill等待匹配

主要内部类

static final class SNode {
    volatile SNode next;  // 下一个节点      // next node in stack
    volatile SNode match; // 匹配节点      // the node matched to this
    volatile Thread waiter; // 对应的线程    // to control park/unpark
    Object item;          // 元素       // data; or null for REQUESTs
    int mode; // 节点类型
    // Note: item and mode fields don't need to be volatile
    // since they are always written before, and read after,
    // other volatile/atomic operations.

    SNode(Object item) {
        this.item = item;
    }

    boolean casNext(SNode cmp, SNode val) {
        return cmp == next && // 原子更新next节点
            UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
    }

    /**
     * Tries to match node s to this node, if so, waking up thread.
     * Fulfillers call tryMatch to identify their waiters.
     * Waiters block until they have been matched.
     *
     * @param s the node to match
     * @return true if successfully matched to s
     */
    boolean tryMatch(SNode s) {
        if (match == null && // 尝试匹配
            UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) {
            Thread w = waiter;
            if (w != null) {    // waiters need at most one unpark
                waiter = null;
                LockSupport.unpark(w); // 匹配成功,唤醒线程
            }
            return true;
        }
        return match == s;
    }

    /**
     * Tries to cancel a wait by matching node to itself.
     */
    /**
     * 尝试取消
     */
    void tryCancel() {
        UNSAFE.compareAndSwapObject(this, matchOffset, null, this); // 把match原子更新成自己
    }

    boolean isCancelled() { // 如果match等于自己,说明已经取消
        return match == this;
    }

    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE; // 魔法类
    private static final long matchOffset; // match偏移量
    private static final long nextOffset; // next偏移量

    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = SNode.class;
            matchOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("match"));
            nextOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("next"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

不论是put还是take操作,内部都是调用transfer方法,下面咱们就重点分析下此方法。

transfer

E transfer(E e, boolean timed, long nanos) {
    /*
     * Basic algorithm is to loop trying one of three actions:
     *
     * 1. If apparently empty or already containing nodes of same
     *    mode, try to push node on stack and wait for a match,
     *    returning it, or null if cancelled.
     *
     * 2. If apparently containing node of complementary mode,
     *    try to push a fulfilling node on to stack, match
     *    with corresponding waiting node, pop both from
     *    stack, and return matched item. The matching or
     *    unlinking might not actually be necessary because of
     *    other threads performing action 3:
     *
     * 3. If top of stack already holds another fulfilling node,
     *    help it out by doing its match and/or pop
     *    operations, and then continue. The code for helping
     *    is essentially the same as for fulfilling, except
     *    that it doesn't return the item.
     */

    SNode s = null; // constructed/reused as needed
    int mode = (e == null) ? REQUEST : DATA; // 如果元素是null,说明是消费者,反之是生产者

    for (;;) { // 自旋
        SNode h = head; // 当前的头结点,让h指向当前的头结点
        if (h == null || h.mode == mode) {  // 如果头结点为null或者头结点的模式和当前节点模式相同
            if (timed && nanos <= 0) {      // 如果设置了超时且已经超时
                if (h != null && h.isCancelled()) // head节点已经取消
                    casHead(h, h.next);     // 把当前head的next结点原子更新为head节点
                else
                    return null;
            } else if (casHead(h, s = snode(s, e, h, mode))) { // 把s(如果s还未创建,先创建s节点)节点原子更新为head节点
                SNode m = awaitFulfill(s, timed, nanos); // 挂起,等待被匹配
                if (m == s) {               // 从awaitFulfill方法返回之后,判断节点有没有被取消
                    clean(s); // 节点被取消,清理节点
                    return null;
                }
                if ((h = head) != null && h.next == s) // 匹配成功,栈顶的两个节点需要出队了
                    casHead(h, s.next);     // head节点指向s的next,相当于出队栈的头两个节点
                /**
                 * 这里特别说明一下:
                 * 第一种可能:如果栈顶节点是消费线程,则消息线程会等待生产线程来匹配,这里返回的元素是生产者的元素
                 * 第二种可能:如果栈顶节点是生产线程,则生产线程会等待消费线程来匹配,这里返回的元素是当前s(也是生产者)的元素
                 */
                return (E) ((mode == REQUEST) ? m.item : s.item);
            }
        } else if (!isFulfilling(h.mode)) { // head结点是否正在匹配中
            if (h.isCancelled())            // head节点是否取消
                casHead(h, h.next);         // 原子更新head节点为head的next(栈顶节点出队)
            // // 把s(如果s还未创建,先创建s节点,节点模式是匹配节点)节点原子更新为head节点
            else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                for (;;) { // 又一个自旋
                    SNode m = s.next;       // s 的next节点
                    if (m == null) {        // 如果是null,说明没有等待匹配的节点了
                        casHead(s, null);   // 头部指向null,栈已经空了
                        s = null;           // use new node next time
                        break;              // restart main loop
                    }
                    SNode mn = m.next;
                    if (m.tryMatch(s)) { // m 尝试匹配s(head指向的节点)
                        casHead(s, mn);     // 匹配成功,栈的前两个节点出队
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    } else     // 没有匹配成功,有可能第二个节点已经取消了
                        s.casNext(m, mn);   // 把head节点的next指向取消节点(m)的next
                }
            }
        } else {                            // 走到这里说明head节点正在匹配
            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))          // 帮助m节点匹配
                    casHead(h, mn);         // 匹配成功,栈头两个节点出队
                else                        // 没有匹配成功,有可能第二个节点已经取消了
                    h.casNext(m, mn);       // 把head节点的next指向取消节点(m)的next
            }
        }
    }
}
  1. 如果head是null或者head节点模式和当前节点模式相同则执行入队操作
  2. 当模式不相同,判断head节点是不是fulfill,如果不是fulfill节点则进行匹配操作
  3. head节点正在匹配中,就帮助头结点匹配

awaitFulfill

SNode awaitFulfill(SNode s, boolean timed, long nanos) {
    /*
     * When a node/thread is about to block, it sets its waiter
     * field and then rechecks state at least one more time
     * before actually parking, thus covering race vs
     * fulfiller noticing that waiter is non-null so should be
     * woken.
     *
     * When invoked by nodes that appear at the point of call
     * to be at the head of the stack, calls to park are
     * preceded by spins to avoid blocking when producers and
     * consumers are arriving very close in time.  This can
     * happen enough to bother only on multiprocessors.
     *
     * The order of checks for returning out of main loop
     * reflects fact that interrupts have precedence over
     * normal returns, which have precedence over
     * timeouts. (So, on timeout, one last check for match is
     * done before giving up.) Except that calls from untimed
     * SynchronousQueue.{poll/offer} don't check interrupts
     * and don't wait at all, so are trapped in transfer
     * method rather than calling awaitFulfill.
     */
    final long deadline = timed ? System.nanoTime() + nanos : 0L; // 如果设置了超时,deadline记录多久之后会超时,否则是0
    Thread w = Thread.currentThread(); // 当前线程
    /**
     * 如果当前节点是head节点或者head结点是null或者head节点正在匹配中则自旋等待一定的次数(
     *  如果设置了超时:
     *  当cpu个数小于2,不自旋
     *  当cpu个数大于2,自旋32次
     *  如果没设置超时:
     *  当cpu个数小于2,不自选
     *  当cpu个数大于2,自旋maxTimedSpins * 16次
     * )否则不自旋等待
     *
     * 自旋的目的:如果当前节点就是head节点或者head节点正在匹配中,可能马上就会轮到当前节点匹配出队,这种自旋的消耗是值得的
     *
     */
    int spins = (shouldSpin(s) ?
                 (timed ? maxTimedSpins : maxUntimedSpins) : 0); // 自旋的次数
    for (;;) {
        if (w.isInterrupted()) // 如果线程已经中断
            s.tryCancel(); // s节点设置为取消,也就是把s的match指向自己
        SNode m = s.match;
        if (m != null) // 如果有节点匹配(有可能是超时取消或者中断),则返回
            return m;
        if (timed) { // 如果设置了超时
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) { // 已经超时
                s.tryCancel(); // 尝试设置为取消
                continue;
            }
        }
        if (spins > 0) // 自旋次数是否大于0
            spins = shouldSpin(s) ? (spins-1) : 0; // 如果还需要自旋则自旋次数减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) // 如果超时剩余时间还大于1000纳秒则挂起,否则没有必要挂起了
            LockSupport.parkNanos(this, nanos); // 超时挂起
    }
}

shouldSpin

 boolean shouldSpin(SNode s) {
    SNode h = head;
    return (h == s || h == null || isFulfilling(h.mode));
}

上面的方法相对比较简单,就是判断当前节点是不是head节点或者head节点是不是正在匹配中,如果成立且cpu个数大于2,则自旋等待一段时间,当有多个cpu时自旋的消耗要远低于线程挂起,因为挂起线程需要用户态和内核态的切换。

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; // s的下一个节点
    if (past != null && past.isCancelled()) // 如果s的next节点也取消了
        past = past.next; // past指向s next的next节点,目的就是找到一个结束节点


    SNode p;
    while ((p = head) != null && p != past && p.isCancelled()) // 如果head节点也取消了,找到一个未取消的作为head节点
        casHead(p, p.next);

    // Unsplice embedded nodes
    while (p != null && p != past) { // 从head节点开始到past结束遍历
        SNode n = p.next; // 下一个节点
        if (n != null && n.isCancelled()) // 如果已经取消
            p.casNext(n, n.next); // 指向取消节点的下一个节点,有可能下一个节点也取消了,p不变继续遍历
        else
            p = n; // head节点的下个节点正常,遍历下一个节点
    }
}

参考

juejin.cn/post/684490…