本文我们一起来分析一下 SynchronousQueue 的设计与实现。不同于前面介绍的一系列线程安全队列,SynchronousQueue 从真正意义上来说并不能算是一个队列,而将其理解为一个用于线程之间通信的组件更为恰当。SynchronousQueue 没有容量的概念,一个线程在执行完入队列操作之后,必须等待另外一个线程与之匹配完成出队列后方可继续再次入队列,反之亦然。此外,有别于我们通常理解的队列中的结点只承载元素,SynchronousQueue 中的结点还需要附着对应的操作线程,这些线程在对应的结点上等待被匹配(fulfill)。
SynchronousQueue 实现自 BlockingQueue 接口,底层基于 LockSupport 工具类 实现线程的阻塞和唤醒操作,并依赖 CAS 保证线程安全。在构造 SynchronousQueue 对象时,允许通过参数指定是否启用公平模式。SynchronousQueue 基于 Dual Stack 数据结构实现非公平的线程通信,基于 Dual Queue 数据结构实现公平的线程通信。SynchronousQueue 的公平模式因为减少了线程之间的冲突,在竞争频繁的场景下反而具备更高的性能,而非公平模式能够更好的维持线程局部性(thread locality),减少线程上下文切换的开销。
SynchronousQueue 示例
本小节我们以“生产者-消费者”示例演示 SynchronousQueue 的基本使用,在示例中我们设置了一个生产者和两个消费者,以展示 SynchronousQueue 公平性特征。示例实现如下(省略了异常处理):
private static BlockingQueue<Integer> queue = new SynchronousQueue<>(true);
private static class Producer implements Runnable {
@Override
public void run() {
int count = 0;
while (count < 10) {
int val = count++;
System.out.println("Producer produce: " + val);
queue.put(val);
TimeUnit.SECONDS.sleep(1);
}
}
}
private static class Consumer implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Consumer " + Thread.currentThread().getName() + " consume: " + queue.take());
}
}
}
public static void main(String[] args) {
Thread producer = new Thread(new Producer());
Thread consumer1 = new Thread(new Consumer());
Thread consumer2 = new Thread(new Consumer());
consumer1.setName("A");
consumer2.setName("B");
producer.start();
consumer1.start();
consumer2.start();
}
运行输出如下:
Producer produce: 0
Consumer A consume: 0
Producer produce: 1
Consumer A consume: 1
Producer produce: 2
Consumer B consume: 2
Producer produce: 3
Consumer A consume: 3
Producer produce: 4
Consumer B consume: 4
Producer produce: 5
Consumer A consume: 5
Producer produce: 6
Consumer B consume: 6
Producer produce: 7
Consumer A consume: 7
Producer produce: 8
Consumer B consume: 8
Producer produce: 9
Consumer A consume: 9
可以看到,当生产者往 SynchronousQueue 中插入一个元素之后,生产者线程会等待消费者完成消费,而消费者线程在完成消费之后会等待生产者生产。SynchronousQueue 的公平性特性尽可能保证了消费者 A 和 B 能够交替执行消费操作。
在上述示例中,如果我们将 Producer 入队列的方法由 put 改为 offer,那么在 Consumer 入队列成功之前,Producer 始终不能入队列成功,这对于一般的队列而言显得有些奇怪。实际上,这里说的不能成功入队列不够准确,要知道 offer 是一类带有超时机制的方法,也就是说当 Producer 在将某个元素执行入队列之后,它希望有一个 Consumer 能够在自己期望的时间内与该元素进行匹配,否则就只能返回 false,从表象上来看就是没有入队列成功。实际应用中我们需要考虑此类表象是否符合自己的业务场景,如果不满足则可以考虑使用 put 方法执行入队列操作。
核心方法实现
SynchronousQueue 实现自 BlockingQueue 接口,但并未对接口中声明的方法全部支持,例如 SynchronousQueue 的 SynchronousQueue#peek 方法就始终返回 null,在使用时推荐先阅读 API 文档,避免影响程序的正确性。本文主要分析 SynchronousQueue 的实现机制,所以下面重点来看一下 SynchronousQueue 已实现的出队列和入队列操作。
前面我们提及到 SynchronousQueue 内部基于 Dual Stack 和 Dual Queue 数据结构实现,在 SynchronousQueue 中定义了一个 Transferer 抽象类,该类抽象了 Dual Stack 和 Dual Queue 数据结构的实现,定义如下:
abstract static class Transferer<E> {
abstract E transfer(E e, boolean timed, long nanos);
}
SynchronousQueue 的出队列和入队列操作均委托给 Transferer#transfer 方法执行(如下),该方法接收 3 个参数,其中参数 e 表示待添加到队列中的元素值,对于出队列操作来说,e 始终等于 null;参数 timed 用于设置当前操作是否具备超时策略,如果是则需要使用参数 nanos 参数指定超时时间。
SynchronousQueue#put(E e) -> transferer.transfer(e, false, 0)
SynchronousQueue#offer(E) -> transferer.transfer(e, true, 0)
SynchronousQueue#offer(E, long, TimeUnit) -> transferer.transfer(e, true, unit.toNanos(timeout))
SynchronousQueue#take -> transferer.transfer(null, false, 0)
SynchronousQueue#poll() -> transferer.transfer(null, true, 0)
SynchronousQueue#poll(long, TimeUnit) -> transferer.transfer(null, true, unit.toNanos(timeout))
针对 Dual Stack 和 Dual Queue 数据结构,SynchronousQueue 分别定义了 TransferStack 和 TransferQueue 实现类,下面的小节将针对这两个类的实现机制展开分析。
在开始之前,我们先对 匹配 一词在 SynchronousQueue 中的含义进行解释,在下面的章节中将多次提及匹配的概念。我们大致已经了解到 SynchronousQueue 在内部基于栈或队列实现线程间的交互,以“生产者-消费者”为例,如果使用的是栈结构(队列亦如此),当生产者往 SynchronousQueue 中插入一个元素时,该生产者线程在插入成功之后并不会立即返回,而是等待消费者前来消费。当消费者执行消费时发现栈上正好有生产者在等待,于是执行消费逻辑,也称为开始执行匹配(fulfill)进程,将当前消费者与生产者匹配成一对儿纷纷出栈https://www.dufawa.com/
Dual Stack
针对 Dual Stack 数据结构,SynchronousQueue 实现了 TransferStack 类。TransferStack 继承自 Transferer 抽象类,并定义了 SNode 类描述栈上的结点。针对结点的运行模式,TransferStack 定义了 3 个 int 类型的常量字段予以描述,如下:
REQUEST:标识未匹配的消费者结点。
DATA:标识未匹配的生产者结点。
FULFILLING:标识结点正在执行匹配操作。
栈在运行期间要么为空,要么存放着一个或多个未匹配的消费者结点或生产者结点,对应的消费者或生产者线程依附在具体的结点上等待。一个栈上不可能同时共存未匹配的消费者结点和未匹配的生产者结点,也就是说同一时间栈上所有结点的运行模式(即 SNode#mode 字段值)都应该是一致的,除了栈顶结点可能会因为正在执行匹配进程而附加 FULFILLING 状态。
SNode 类的字段定义如下:
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
/** 结点元素值,如果是消费者结点则为 null */
Object item; // data; or null for REQUESTs
/**
* 结点运行模式:
* - 0:代表消费者结点
* - 1:代表生产者结点
* - (2 | 0) or (2 | 1):代表结点正在或已被匹配
*/
int mode;
// ... 省略方法实现
}
各字段的含义如代码注释,我们将在下面分析 TransferStack#transfer
方法实现时一并分析 SNode 中定义的方法,并对各个字段的含义结合具体场景做进一步介绍。
前面在介绍 Transferer 抽象类时,我们知道该抽象类仅声明了一个方法,即 Transferer#transfer
方法,该方法也是整个 SynchronousQueue 中最核心的实现。在开始分析读法网 TransferStack 之于该方法的实现之前,我们先从整体出发,感知一下 TransferStack 的运行流程。
以“生产者-消费者”为例,假设当前有 3 个生产者依次执行往 SynchronousQueue 中插入元素,执行的顺序为 1 -> 2 -> 3
,则入栈之后得到的栈结构如下:
3 -> 2 -> 1 -> null
↓
head
入栈后的 3 个生产者线程将在栈对应结点上等待。如果来了一个消费者执行出队列操作,此时消费者将与 head 结点上的生产者进行匹配,匹配成功之后得到的栈结构如下:
2 -> 1 -> null
↓
head
此时剩下的生产者线程将继续等待,期间可以允许新的消费者出队列,也可以允许新的生产者入队列。
上述过程就是 TransferStack#transfer
方法的核心执行逻辑,对此有了一个大概的感知之后,下面来深入分析 TransferStack#transfer
方法的具体实现。实际上在 TransferStack#transfer
方法的开头,作者已经对整个方法的运行流程给出了直观的概括,摘录如下:
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.
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:
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.
方法 TransferStack#transfer
实现如下:
E transfer(E e, boolean timed, long nanos) {
SNode s = null; // constructed/reused as needed
// 操作模式判定,如果为 null 则说明当前是出队列操作,否则说明是入队列操作
int mode = (e == null) ? REQUEST : DATA;
for (; ; ) {
SNode h = head;
// 1. 如果栈为空,或者包含相同模式的结点,将结点入栈等待匹配
if (h == null || h.mode == mode) { // empty or same-mode
// 如果设置超时且到期
if (timed && nanos <= 0) { // can't wait
// 如果 head 结点被取消,则后移 head 指针
if (h != null && h.isCancelled()) {
this.casHead(h, h.next); // pop cancelled node
} else {
// 否则返回 null
return null;
}
}
// 否则,说明当前线程需要在栈上等待,先创建一个结点入栈,之后对应的线程会在该结点上等待
else if (this.casHead(h, s = snode(s, e, h, mode))) {
// 等待结点被匹配或取消,返回的是与当前结点匹配的结点,或者结点自己(即结点被取消)
SNode m = this.awaitFulfill(s, timed, nanos);
// 如果返回的是结点自己,则说明是被取消了
if (m == s) { // wait was cancelled
// 清理无效结点
this.clean(s);
return null;
}
/* 当前结点被匹配了 */
// 与 s 匹配的结点就是 head 结点,将 s 和 m 出栈,这里只是一个优化,不影响程序执行的正确性
if ((h = head) != null && h.next == s) {
this.casHead(h, s.next); // help s's fulfiller
}
// 如果是出队列则返回匹配结点的元素值,如果是入队列则返回新添加的结点元素值
return (E) ((mode == REQUEST) ? m.item : s.item);
}
}
// 2. 栈中包含互补模式的结点,且 head 结点不处于 FULFILLING 状态,执行匹配操作
else if (!isFulfilling(h.mode)) { // try to fulfill
// 头结点已经被取消,则后移 head 指针后重试
if (h.isCancelled()) { // already cancelled
this.casHead(h, h.next); // pop and retry
}
// 入队一个带有 FULFILLING 标志的新结点 s,同一时间栈中最多只有一个带有 FULFILLING 标志的结点,且该结点一定是 head 结点
else if (this.casHead(h, s = snode(s, e, h, FULFILLING | mode))) {
for (; ; ) { // loop until matched or waiters disappear
// 获取本次与 s 结点执行匹配的结点,也就是 s 的 next 结点
SNode m = s.next; // m is s's match
// 如果待匹配的结点为 null,说明已经被其它线程取消
if (m == null) { // all waiters are gone
// 将结点 s 出队列,并退出循环
this.casHead(s, null); // pop fulfill node
s = null; // use new node next time
break; // restart main loop
}
// 如果待匹配的结点不为 null,则尝试执行匹配
SNode mn = m.next;
if (m.tryMatch(s)) { // 尝试将结点 m 的 match 指针指向结点 s
// 匹配成功,修改头结点为已匹配结点 m 的 next 结点
this.casHead(s, mn); // pop both s and m
// 如果是出队列则返回已匹配结点的元素值,如果是入队列则返回新添加的结点元素值
return (E) ((mode == REQUEST) ? m.item : s.item);
} else { // lost match
// 匹配失败,说明结点 m 被取消,继续尝试匹配 m 的 next 结点
s.casNext(m, mn); // help unlink
}
}
}
}
// 3. 栈中包含互补模式的结点,且 head 结点处于 FULFILLING 状态
else { // help a fulfiller
SNode m = h.next; // m is h's match
if (m == null) { // waiter is gone
this.casHead(h, null); // pop fulfilling node
} else {
SNode mn = m.next;
if (m.tryMatch(h)) { // help match
this.casHead(h, mn); // pop both h and m
} else { // lost match
h.casNext(m, mn); // help unlink
}
}
}
}
}
上述实现中 for 循环内部的 if ... else if ... else
控制结构分别对应作者给出的 3 段注释(已在代码中标出),其中场景 3 主要是对场景 2 的辅助,下面重点分析场景 1 和场景 2 的实现和执行流程。
首先来看一下 场景 1 ,此时栈为空,或者栈中等待的线程运行模式与当前线程的运行模式相同,此时需要将结点入栈,并让当前线程在结点上等待。执行流程可以概括为:
- 如果设置了超时且已经到期,则顺带判断 head 结点是否被取消,如果是则后移 head 指针并进入下一轮循环,否则返回 null;
- 否则新建一个包含待添加元素 e 的结点入栈,并执行
TransferStack#awaitFulfill
方法让当前线程在该结点上等待匹配(或被取消); - 如果在等待期间被取消,则清理栈上的无效结点,并返回 null;
- 否则说明结点被成功匹配,如果当前线程是消费者线程则返回匹配结点的元素值,如果当前线程是生产者线程则返回刚刚添加的元素值。
下面利用图示演示上述执行流程。假设当前操作线程是一个生产者,期望将元素 3 插入到 SynchronousQueue 中,并且当前栈中已经包含两个处于等待状态的生产者(如下图 1 所示)。因为当前线程与栈中等待的线程模式相同(均为 DATA),所以新建一个元素值为 3 的结点入栈(如下图 2 所示),并让当前线程在结点上等待。