SynchronousQueue是队列中最特殊的队列,它本身没有容量大小,将一个元素放入到队列中后无法立刻返回。直到其他线程将放入的元素消费掉才能够返回。
SynchronousQueue在消息队列技术中间件中被大量使用,其内部同时使用两种数据结构实现队列,分别是FIFO的队列和FILO的堆栈。分别对应两个内部类TransferQueue和TransferStack,这两个类中都使用transfer方法一站式解决了数据的存放和取出功能。SynchronousQueue的put和take方法统一调用的都是这两个类的transfer方法。
1. 整体架构
内部类的transfer方法和SynchronousQueue的take和put方法的调用关系如下,
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.源码解析
非公平堆栈
堆栈的示意图如下,
对于堆栈而言,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)
- 判断是put还是take方法,之后进入自旋状态
SNode s = null;
// e 为空,说明是 take 方法(mode=0)
// 不为空是 put 方法(mode=1)
int mode = (e == null) ? REQUEST : DATA;
- 判断栈头元素是否为null或者栈头元素的操作(SNode实例对象的mode)与当前操作的mode是否一致。
for (;;) {
SNode h = head; // h指向栈头
if (h == null || h.mode == mode) {
......;
}
}
- 如果当前操作与栈头元素操作一致,则走步骤3。
- 如果当前操作与栈头元素操作不一致,则走步骤4。
- 判断是否有设置超时时间,如果设置且已经超出时间,直接返回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
方法)。
- 如果栈头已经是阻塞的,需要被唤醒,则判断当前操作能否唤醒栈头(对应
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
- 栈头被唤醒后,将唤醒自己的节点的信息返回。
节点阻塞方法
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从队列中take数据,因为队列为空,所以线程1被阻塞成为阻塞线程A
- 线程2往队列中put数据,从队尾往前寻找第一个阻塞节点,如果找不到就进入阻塞队列。假设找到阻塞线程A,则线程2将put的数据赋予A的item变量。并唤醒线程1
- 线程1被唤醒后从线程A中取出item变量,返回结果