- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
本文分享自华为云社区《Synchronous Queue 源码解析》,作者: JavaEdge。
SynchronousQueue 是一种特立独行的队列,其本身是没有容量的,比如调用者放一个数据到队列中,调用者是不能够立马返回的,调用者必须等待别人把我放进去的数据消费掉了,才能够返回。SynchronousQueue 在 MQ 中被大量使用,本文就让我们从源码来看下 SynchronousQueue 到底是如何实现这种功能的呢。
不像ArrayBlockingQueue
、LinkedBlockingDeque
之类的阻塞队列使用AQS实现并发,SynchronousQueue
直接使用CAS操作实现数据的安全访问,因此源码中充斥着大量的CAS代码。
SynchronousQueue 的整体设计比较抽象,在内部抽象出了两种算法实现,一种是先入先出的队列,一种是后入先出的堆栈,两种算法被两个内部类实现,而直接对外的 put,take 方法的实现就非常简单,都是直接调用两个内部类的 transfer 方法进行实现,整体的调用关系如下图所示:
1.1 类注释
队列不存储数据,所以没有大小,也无法迭代;
插入操作的返回必须等待另一个线程完成对应数据的删除操作,反之亦然;
队列由两种数据结构组成,分别是后入先出的堆栈和先入先出的队列,堆栈是非公平的,队列是公平的。
第二点是如何做到的?堆栈又是如何实现的呢?接下来我们一点一点揭晓。
1.2 类图
SynchronousQueue
整体类图和 LinkedBlockingQueue
相似,都实现了 BlockingQueue
接口,但因为其不储存数据结构,有一些方法是没有实现的,比如说 isEmpty、size、contains、remove 和迭代等方法,这些方法都是默认实现,如下截图:
SynchronousQueue 底层结构和其它队列完全不同,有着独特的两种数据结构:队列和堆栈,我们一起来看下数据结构:
// 堆栈和队列共同的接口
// 负责执行 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>();
}
从源码中我们可以得到几点:
堆栈和队列都有一个共同的接口,叫做 Transferer,该接口有个方法:transfer,该方法很神奇,会承担 take 和 put 的双重功能;
在我们初始化的时候,是可以选择是使用堆栈还是队列的,如果你不选择,默认的就是堆栈,类注释中也说明了这一点,堆栈的效率比队列更高。
接下来我们来看下堆栈和队列的具体实现。
首先我们来介绍下堆栈的整体结构,如下:
从上图中我们可以看到,我们有一个大的堆栈池,池的开口叫做堆栈头,put 的时候,就往堆栈池中放数据。take 的时候,就从堆栈池中拿数据,两者操作都是在堆栈头上操作数据,从图中可以看到,越靠近堆栈头,数据越新,所以每次 take 的时候,都会拿到堆栈头的最新数据,这就是我们说的后入先出,也就是非公平的。
图中 SNode 就是源码中栈元素的表示,我们看下源码:
volatile SNode next
volatile SNode match
volatile Thread waiter
Object item
操作的对象都是堆栈头,虽然两者的一个是从堆栈头拿数据,一个是放数据,但底层实现的方法却是同一个,源码如下:
transfer 方法思路比较复杂,因为 take 和 put 两个方法都揉在了一起A
@SuppressWarnings("unchecked")
E transfer(E e, boolean timed, long nanos) {
SNode s = null; // constructed/reused as needed
// e 为空: take 方法,非空: put 方法
int mode = (e == null) ? REQUEST : DATA;
// 自旋
for (;;) {
// 头节点情况分类
// 1:为空,说明队列中还没有数据
// 2:非空,并且是 take 类型的,说明头节点线程正等着拿数据
// 3:非空,并且是 put 类型的,说明头节点线程正等着放数据
SNode h = head;
// 栈头为空,说明队列中还没有数据。
// 栈头非空且栈头的类型和本次操作一致
// 比如都是 put,那么就把本次 put 操作放到该栈头的前面即可,让本次 put 能够先执行
if (h == null || h.mode == mode) { // empty or same-mode
// 设置了超时时间,并且 e 进栈或者出栈要超时了,
// 就会丢弃本次操作,返回 null 值。
// 如果栈头此时被取消了,丢弃栈头,取下一个节点继续消费
if (timed && nanos <= 0) { // 无法等待
// 栈头操作被取消
if (h != null && h.isCancelled())
// 丢弃栈头,把栈头的后一个元素作为栈头
casHead(h, h.next); // 将取消的节点弹栈
// 栈头为空,直接返回 null
else
return null;
// 没有超时,直接把 e 作为新的栈头
} else if (casHead(h, s = snode(s, e, h, mode))) {
// e 等待出栈,一种是空队列 take,一种是 put
SNode m = awaitFulfill(s, timed, nanos);
if (m == s) { // wait was cancelled
clean(s);
return null;
}
// 本来 s 是栈头的,现在 s 不是栈头了,s 后面又来了一个数,把新的数据作为栈头
if ((h = head) != null && h.next == s)
casHead(h, s.next); // help s's fulfiller
return (E) ((mode == REQUEST) ? m.item : s.item);
}
// 栈头正在等待其他线程 put 或 take
// 比如栈头正在阻塞,并且是 put 类型,而此次操作正好是 take 类型,走此处
} else if (!isFulfilling(h.mode)) { // try to fulfill
// 栈头已经被取消,把下一个元素作为栈头
if (h.isCancelled()) // already cancelled
casHead(h, h.next); // pop and retry
// snode 方法第三个参数 h 代表栈头,赋值给 s 的 next 属性
else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
for (;;) { // loop until matched or waiters disappear
// m 就是栈头,通过上面 snode 方法刚刚赋值
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;
// tryMatch 非常重要的方法,两个作用:
// 1 唤醒被阻塞的栈头 m,2 把当前节点 s 赋值给 m 的 match 属性
// 这样栈头 m 被唤醒时,就能从 m.match 中得到本次操作 s
// 其中 s.item 记录着本次的操作节点,也就是记录本次操作的数据
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
}
}
} else { // help a fulfiller
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)) // help match
casHead(h, mn); // pop both h and m
else // lost match
h.casNext(m, mn); // help unlink
}
}
}
}
总结一下操作思路:
在整个过程中,有一个节点阻塞的方法,源码如下:
当一个 节点/线程 将要阻塞时,它会设置其 waiter 字段,然后在真正 park 之前至少再检查一次状态,从而涵盖了竞争与实现者的关系,并注意到 waiter 非空,因此应将其唤醒。
当由出现在调用点位于堆栈顶部的节点调用时,对停放的调用之前会进行旋转,以避免在生产者和消费者及时到达时阻塞。 这可能只足以在多处理器上发生。
从主循环返回的检查顺序反映了这样一个事实,即优先级: 中断 > 正常的返回 > 超时。 (因此,在超时时,在放弃之前要进行最后一次匹配检查。)除了来自非定时SynchronousQueue的调用。{poll / offer}不会检查中断,根本不等待,因此陷入了转移方法中 而不是调用awaitFulfill。
/**
* 旋转/阻止,直到节点s通过执行操作匹配。
* @param s 等待的节点
* @param timed true if timed wait
* @param nanos 超时时间
* @return 匹配的节点, 或者是 s 如果被取消
*/
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 操作,自旋次数后,仍无其他线程 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; // establish waiter so can park next iter
else if (!timed)
// 通过 park 进行阻塞,这个我们在锁章节中会说明
LockSupport.park(this);
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
可以发现其阻塞策略,并不是一上来就阻塞住,而是在自旋一定次数后,仍然没有其它线程来满足自己的要求时,才会真正的阻塞。
队列的实现策略通常分为公平模式和非公平模式,本文我们重点介绍公平模式。
volatile QNode next
volatile Object item
// CAS’ed to or from nullvolatile Thread waiter
// to control park/unparkfinal boolean isData
公平队列主要使用的是 TransferQueue 内部类的 transfer 方法,看源码:
E transfer(E e, boolean timed, long nanos) {
QNode s = null; // constructed/reused as needed
// true : put false : get
boolean isData = (e != null);
for (;;) {
// 队列头和尾的临时变量,队列是空的时候,t=h
QNode t = tail;
QNode h = head;
// tail 和 head 没有初始化时,无限循环
// 虽然这种 continue 非常耗cpu,但感觉不会碰到这种情况
// 因为 tail 和 head 在 TransferQueue 初始化的时候,就已经被赋值空节点了
if (t == null || h == null)
continue;
// 首尾节点相同,说明是空队列
// 或者尾节点的操作和当前节点操作一致
if (h == t || t.isData == isData) {
QNode tn = t.next;
// 当 t 不是 tail 时,说明 tail 已经被修改过了
// 因为 tail 没有被修改的情况下,t 和 tail 必然相等
// 因为前面刚刚执行赋值操作: t = tail
if (t != tail)
continue;
// 队尾后面的值还不为空,t 还不是队尾,直接把 tn 赋值给 t,这是一步加强校验。
if (tn != null) {
advanceTail(t, tn);
continue;
}
//超时直接返回 null
if (timed && nanos <= 0) // can't wait
return null;
//构造node节点
if (s == null)
s = new QNode(e, isData);
//如果把 e 放到队尾失败,继续递归放进去
if (!t.casNext(null, s)) // failed to link in
continue;
advanceTail(t, s); // swing tail and wait
// 阻塞住自己
Object x = awaitFulfill(s, e, timed, nanos);
if (x == s) { // wait was cancelled
clean(t, s);
return null;
}
if (!s.isOffList()) { // not already unlinked
advanceHead(t, s); // unlink if head
if (x != null) // and forget fields
s.item = s;
s.waiter = null;
}
return (x != null) ? (E)x : e;
// 队列不为空,并且当前操作和队尾不一致
// 也就是说当前操作是队尾是对应的操作
// 比如说队尾是因为 take 被阻塞的,那么当前操作必然是 put
} else { // complementary-mode
// 如果是第一次执行,此处的 m 代表就是 tail
// 也就是这行代码体现出队列的公平,每次操作时,从头开始按照顺序进行操作
QNode m = h.next; // node to fulfill
if (t != tail || m == null || h != head)
continue; // inconsistent read
Object x = m.item;
if (isData == (x != null) || // m already fulfilled
x == m || // m cancelled
// m 代表栈头
// 这里把当前的操作值赋值给阻塞住的 m 的 item 属性
// 这样 m 被释放时,就可得到此次操作的值
!m.casItem(x, e)) { // lost CAS
advanceHead(h, m); // dequeue and retry
continue;
}
// 当前操作放到队头
advanceHead(h, m); // successfully fulfilled
// 释放队头阻塞节点
LockSupport.unpark(m.waiter);
return (x != null) ? (E)x : e;
}
}
}
线程被阻塞住后,当前线程是如何把自己的数据传给阻塞线程的。
假设线程 1 从队列中 take 数据 ,被阻塞,变成阻塞线程 A 然后线程 2 开始往队列中 put 数据 B,大致的流程如下:
在这个过程中,公平主要体现在,每次 put 数据的时候,都 put 到队尾上,而每次拿数据时,并不是直接从堆头拿数据,而是从队尾往前寻找第一个被阻塞的线程,这样就会按照顺序释放被阻塞的线程。
公平模式下,底层实现使用的是 TransferQueue
队列,它有一个head和tail指针,用于指向当前正在等待匹配的线程节点。
初始化时,TransferQueue的状态如下:
以上便是公平模式下,SynchronousQueue的实现模型。总结下来就是:队尾匹配队头出队,先进先出,体现公平原则。
volatile SNode next
volatile Object item
// data; or null for REQUESTsvolatile Thread waiter
还是使用跟公平模式下一样的操作流程,对比两种策略下有何不同。
非公平模式底层的实现使用的是TransferStack,
一个栈,实现中用head指针指向栈顶,接着我们看看它的实现模型:
可以从上面流程看出,虽然put1线程先入栈了,但是却是后匹配,这就是非公平的由来。
SynchronousQueue 源码比较复杂,建议大家进行源码的 debug 来学习源码,为大家准备了调试类:SynchronousQueueDemo,大家可以下载源码自己调试一下,这样学起来应该会更加轻松一点。
SynchronousQueue由于其独有的线程一一配对通信机制,在大部分平常开发中,可能都不太会用到,但线程池技术中会有所使用,由于内部没有使用AQS,而是直接使用CAS,所以代码理解起来会比较困难,但这并不妨碍我们理解底层的实现模型,在理解了模型的基础上,再翻阅源码,就会有方向感,看起来也会比较容易!
创作打卡挑战赛
赢取流量/现金/CSDN周边激励大奖
我遇到一种情况,我需要从某个主题读取(正在进行的)消息并将它们放入另一个 Queue 中。我怀疑我是否需要 jms Queue 或者我可以对内存中的 java Queue 感到满意。我将通过同一 jv
队列也是一种操作受限的线性数据结构,与栈很相似。 01、定义 栈的操作受限表现为只允许在队列的一端进行元素插入操作,在队列的另一端只允许删除操作。这一特性可以总结为先进先出(First In
队列的定义 队列(Queue):先进先出的线性表 队列是仅在队尾进行插入和队头进行删除操作的线性表 队头(front):线性表的表头端,即可删除端 队尾(rear):线性表的表尾端,即可插入端 由于这
Redis专题-队列 首先,想一想 Redis 适合做消息队列吗? 1、消息队列的消息存取需求是什么?redis中的解决方案是什么? 无非就是下面这几点: 0、数据可以顺序读
0. 学习目标 栈和队列是在程序设计中常见的数据类型,从数据结构的角度来讲,栈和队列也是线性表,是操作受限的线性表,它们的基本操作是线性表操作的子集,但从数据类型的角度来讲,它们与线性表又有着巨大的不
我想在 redis + Flask 和 Python 中实现一个队列。我已经用 RQ 实现了这样的查询,如果你有 Flask 应用程序和任务在同一台服务器上工作,它就可以正常工作。我想知道是否有可能创
我正在使用 Laravel 5.1,我有一个大约需要 2 分钟来处理的任务,这个任务特别是生成报告...... 现在,很明显,我不能让用户在我接受用户输入的同一页面上等待 2 分钟,而是我应该在后台处
我正在使用 Azure 队列,并且有多个不同的进程从队列中读取数据。 我的系统的构建方式假设每条消息只读取一次。 这个Microsoft article声称 Azure 队列具有至少一次传送保证,这可
我正在创建一个Thread::Queue元素数组。 我这样做是这样的: for (my $i=0; $i new; } 但是,当我在每个队列中填充这样的元素时 $queues[$index]->enq
我试图了解如何将我的 Mercurial 补丁推送到远程存储库(例如 bitbucket.org),而不必先应用它们(实际上提交它们)。我的动机是在最终完成之前首先对我的工作进行远程备份,并且能够与其
我的本地计算机上有一个 Mercurial 队列补丁,我需要与同事共享该补丁,但我不想将其提交到上游存储库。有没有一种简单的方法可以打包该补丁并与他分享? 最佳答案 mq 将补丁作为不带扩展名的文
Java 中是否有任何类提供与 Queue 相同的功能,但有返回对象的选项,并且不要删除它,只需将其设置在集合末尾? 最佳答案 Queue不直接提供这样的方法。但是,您可以使用 poll 和 add
我在Windows上使用Tortoise svn客户端,我需要能够一次提交来自不同子文件夹的更改文件-一次提交。像在提交之前将文件添加到队列中之类的?我该怎么做? Windows上是否还有另一个svn
好吧,我正在尝试对我的 DSAQueue 类进行单元测试,它显示我的 isEmpty()、isFull() 和 dequeue() 方法失败。 以下是我的 DSAQueue 代码。我认为我的 Dequ
我想尽量减少对传入请求的数据库查询。它目前需要写入 6 个不同的表。在返回响应之前不需要完成处理。因此,我考虑了 laravel 队列,但我想知道我是否也可以摆脱写入队列/作业表所需的单独查询。我可以
我正在学习队列数据结构。我想用链表创建队列。我想编程输出:10 20程序输出:队列为空-1 队列为空-1 我哪里出错了? 代码如下: class Node { int x; Node next
“当工作人员有空时,他们会根据主题的优先级列表从等待请求池中进行选择。在时间 t 到达的所有请求都可以在时间 t 进行分配。如果两名工作人员同时有空,则安排优先权分配给最近的工作最早安排的人。如果仍然
我正在开发一个巨大的应用程序,它使用一些子菜单、模式窗口、提示等。 现在,我想知道在此类应用程序中处理 Esc 和单击外部事件的正确方法。 $(document).keyup(function(e)
所以 如果我有一个队列 a --> b --> NULL; 当我使用函数时 void duplicate(QueueNodePtr pHead, QueueNodePtr *pTail) 它会给 a
我正在尝试为键盘输入实现 FIFO 队列,但似乎无法让它工作。我可以让键盘输入显示在液晶显示屏上,但这就是我能做的。我认为代码应该读取键盘输入并将其插入队列,然后弹出键盘输入并将值读取到液晶屏幕上。有
我是一名优秀的程序员,十分优秀!