队列:SynchronousQueue源码解析


SynchronousQueue是队列中最特殊的队列,它本身没有容量大小,将一个元素放入到队列中后无法立刻返回。直到其他线程将放入的元素消费掉才能够返回。

SynchronousQueue在消息队列技术中间件中被大量使用,其内部同时使用两种数据结构实现队列,分别是FIFO的队列和FILO的堆栈。分别对应两个内部类TransferQueueTransferStack,这两个类中都使用transfer方法一站式解决了数据的存放和取出功能。SynchronousQueue的put和take方法统一调用的都是这两个类的transfer方法。

1. 整体架构

内部类的transfer方法和SynchronousQueue的take和put方法的调用关系如下,
image
SynchronousQueue类的类注释如下,

  • 队列不存储数据,所以没有大小,无法迭代
  • 插入操作必须等待另一个线程完成该数据的消耗后才能返回;反之亦然
  • 队列由两种数据结构构成,FIFO的队列和FILO的堆栈,前者是公平的,后者是非公平的

两种数据结构如下,

 // 堆栈和队列共同的接口, 负责执行 put or take
abstract static class Transferer<E> {
    // e 为空的,会直接返回特殊值,不为空会传递给消费者
    // timed 为 true,说明会有超时时间
    abstract E transfer(E e, boolean timed, long nanos);
}

// 堆栈 后入先出 非公平
// Scherer-Scott 算法
static final class TransferStack<E> extends Transferer<E> {
}

// 队列 先入先出 公平
static final class TransferQueue<E> extends Transferer<E> {
}

private transient volatile Transferer<E> transferer;

// 无参构造器默认为非公平的
public SynchronousQueue(boolean fair) {
    transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}

2.源码解析

非公平堆栈

堆栈的示意图如下,
image
对于堆栈而言,put和take都是对栈头进行操作。SNode类是堆栈中元素类,

static final class SNode {
    volatile SNode next;
    volatile SNode match;
    volatile Thread waiter;
    int mode;
    Object item;             
} 

各成员变量作用,

成员变量说明
next栈的下一个元素,压在当前元素底下
match用于判断唤醒阻塞元素的时机
waiter栈元素阻塞通过线程实现
mode表示该节点的状态是存放还是取出

入栈和出栈

put和take方法调用的都是TransferStack类的transfer方法,该方法的代码比较复杂,分步进行讲解。具体过程如下,

E transfer(E e, boolean timed, long nanos)
  1. 判断是put还是take方法,之后进入自旋状态
SNode s = null; 
// e 为空,说明是 take 方法(mode=0)
// 不为空是 put 方法(mode=1)
 int mode = (e == null) ? REQUEST : DATA;
  1. 判断栈头元素是否为null或者栈头元素的操作(SNode实例对象的mode)与当前操作的mode是否一致。
for (;;) {
    SNode h = head;	// h指向栈头
    if (h == null || h.mode == mode) {
    	......;
    }
}
  • 如果当前操作与栈头元素操作一致,则走步骤3。
  • 如果当前操作与栈头元素操作不一致,则走步骤4。
  1. 判断是否有设置超时时间,如果设置且已经超出时间,直接返回null。否则指向下面的代码,
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);
}
  • 如果栈头为null,将当前操作设置为栈头。
  • 如果栈头不为空,栈头操作与当前操作相同,则也把当前操作设为栈头。

栈头设置完毕后查看是否有线程能够满足自身,如果没有,则将自身阻塞(对应上面的awaitFulfill方法)。

  1. 如果栈头已经是阻塞的,需要被唤醒,则判断当前操作能否唤醒栈头(对应isFulfilling方法)。
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
        }
    }
}
  • 如果可以唤醒,将自身作为一个SNode对象,复制到栈头元素的match属性上,并唤醒栈头元素进入步骤5
  • 如果不能唤醒,走步骤3
  1. 栈头被唤醒后,将唤醒自己的节点的信息返回。

节点阻塞方法

awaitFulfill方法源码如下,

SNode awaitFulfill(SNode s, boolean timed, long nanos) {
    // deadline 死亡时间,如果设置了超时时间的话,死亡时间等于当前时间 + 超时时间,否则就是 0
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    Thread w = Thread.currentThread();
    // 自旋的次数,如果设置了超时时间,会自旋 32 次,否则自旋 512 次。
    // 比如本次操作是 take 操作,自旋1次spins变量-1,仍没有其他线程 put 数据进来
    // 就会阻塞,有超时时间的,会阻塞固定的时间,否则一致阻塞下去
    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;
            }
        }
        // 自选次数-1
        if (spins > 0)
            spins = shouldSpin(s) ? (spins-1) : 0;
        // 把当前线程设置成 waiter,主要是通过线程来完成阻塞和唤醒
        else if (s.waiter == null)
            s.waiter = w; 
        else if (!timed)
            // 通过 park 进行阻塞,在锁章节中会说明
            LockSupport.park(this);
        else if (nanos > spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanos);
    }
}

阻塞的策略是自旋一定次数后再阻塞。

公平队列

公平队列的构成如下,

/** 队列头 */
transient volatile QNode head;
/** 队列尾 */
transient volatile QNode tail;

// 队列的元素
static final class QNode {
    // 当前元素的下一个元素
    volatile QNode next;         
    // 当前元素的值,如果当前元素被阻塞住了,等其他线程来唤醒自己时,其他线程
    // 会把自己 set 到 item 里面
    volatile Object item;         // CAS'ed to or from null
    // 可以阻塞住的当前线程
    volatile Thread waiter;       // to control park/unpark
    // true 是 put,false 是 take
    final boolean isData;
}

入队和出队

E transfer(E e, boolean timed, long nanos) {

    QNode s = null; 
    // true 是 put,false 是 get
    boolean isData = (e != null);

    for (;;) {
        // 队列头和尾的临时变量,队列是空的时候,t=h
        QNode t = tail;
        QNode h = head;
        // tail 和 head 没有初始化时,无限循环
        // 理论上不会碰到这种情况。因为 tail 和 head 在 TransferQueue 初始化的时候,就已经被赋值空节点了
        if (t == null || h == null)
            continue;
        // 首尾节点相同,说明是空队列
        // 或者尾节点的操作和当前节点操作一致
        if (h == t || t.isData == isData) {
            QNode tn = t.next;
            // 当 t 不是 tail 时,说明 tail 已经被修改过了
            if (t != tail)
                continue;
            // 队尾后面的值还不为空,t 还不是队尾,直接把 tn 赋值给t
            // 因为可能出现其他线程加了新队尾元素。这是一步加强校验找到真的队尾。
            if (tn != null) {
                advanceTail(t, tn);
                continue;
            }
            // 超时直接返回 null
            if (timed && nanos <= 0)
                return null;
            // s=null,即之前没创建节点,构造QNode节点
            if (s == null)
                s = new QNode(e, isData);
            //如果把 e 放到队尾失败,继续尝试放进去
            if (!t.casNext(null, s))
                continue;

            advanceTail(t, s);
            // 阻塞住自己
            Object x = awaitFulfill(s, e, timed, nanos);
            if (x == s) {
                clean(t, s);
                return null;
            }

            if (!s.isOffList()) {           // not already unlinked
                advanceHead(t, s);          // unlink if head
                if (x != null)              
                    s.item = s;
                s.waiter = null;
            }
            return (x != null) ? (E)x : e;
        // 队列不为空且当前操作和队尾不一致
        // 也就是说当前操作是队尾是对应的操作
        // 比如说队尾是因为 take 被阻塞的,那么当前操作必然是 put
        } else {                            
            // 如果是第一次执行,此处的 m 代表就是 tail
            // 也就是这行代码体现出队列的公平,每次操作时,从头开始按照顺序进行操作
            QNode m = h.next; 
            if (t != tail || m == null || h != head)
                continue; 

            Object x = m.item;
            if (isData == (x != null) ||    
                x == m || 
                // m 代表栈头
                // 这里把当前的操作值赋值给阻塞住的 m 的 item 属性
                // 这样 m 被释放时,就可得到此次操作的值
                !m.casItem(x, e)) {         // lost CAS
                advanceHead(h, m);          // dequeue and retry
                continue;
            }
            // 当前操作放到队头
            advanceHead(h, m);
            // 释放队头阻塞节点
            LockSupport.unpark(m.waiter);
            return (x != null) ? (E)x : e;
        }
    }
}

TransferQueue的过程如下,假设有两个线程,线程1从队列中take数据,因为队列为空,所以线程1被阻塞成为阻塞线程A。此时线程2往队列中put数据,数据内容为B。

  1. 线程1从队列中take数据,因为队列为空,所以线程1被阻塞成为阻塞线程A
  2. 线程2往队列中put数据,从队尾往前寻找第一个阻塞节点,如果找不到就进入阻塞队列。假设找到阻塞线程A,则线程2将put的数据赋予A的item变量。并唤醒线程1
  3. 线程1被唤醒后从线程A中取出item变量,返回结果
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值