0% found this document useful (0 votes)
84 views

Java编程方法论:响应式Spring Reactor 3设计与实现@Www.cmsblogs.cn

Uploaded by

vhxctm
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
84 views

Java编程方法论:响应式Spring Reactor 3设计与实现@Www.cmsblogs.cn

Uploaded by

vhxctm
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 483

版权信息

COPYRIGHT
书名:Java编程方法论:响应式Spring Reactor 3设计与实现
作者:知秋
出版社:电子工业出版社
出版时间:2020年9月
ISBN:9787121394768
字数:456千字

版权方:电子工业出版社有限公司
版权所有·侵权必究
内容简介
本书主要专注于解读Spring Reactor 3的代码设计与实现。全书
共10章,其中第1、2章将从接口的设计入手,逐渐深入介绍Reactor中
Flux源与订阅者Subscriber的诸多交互实现细节;第3章将通过对调度
器的研究来向读者展示其中的优秀设计,可以帮助读者更好地掌握
Java并发库,同时可以使读者对使用Reactor进行异步编程有更好的认
识;第4章将接触到一些常用的Reactor操作,方便读者写出可重用度
高、逻辑清晰的代码;第5、6、7章将着重分析Reactor中Processor的
设计实现,不仅讲述了它的使用场景,还讲解了其中的内在原理,以
及如何应对未来项目开发过程中可能遇到的种种问题;第8章将介绍并
分析Reactor特别提供的Context,这是Reactor为了应对生产-订阅模
式下的响应式编程在异步环境中对订阅关系上下文进行管理所产生的
问题而给出的解决方案,Spring Framework 5.2中的响应式事务也是
基于它实现的;第9章将主要介绍Reactor中的测试,同时带着读者一
步一步设计实现一个针对Reactor项目的测试库;第10章将主要介绍
Reactor中的调试,可以教会读者根据不同的需求采取不同的调试方
式。
本书适合有Java编程基础的中高级Java开发工程师、想要学习代
码设计思路与技巧的读者、对响应式编程感兴趣的读者阅读。
推荐序一
Towards a More Exciting, More Reactive Tomorrow
Hi, Spring fans!It's an interesting time to be alive.
Today, the possible applications any programmer can build
today are much more numerous than they were when I first
started. The opportunities are so much more than they were
before.It's possible to write code that runs on the server-
side, on the backend, in giant systems, in big data
platforms, in streaming platforms, on mobile phones, in cars,
in the browser, on your watch, on your tablets, on your TVs,
etc., all while using fairly similar languages, and while
using similar skills. All of these destinations are also open
and-often-open-source.It costs almost nothing to build this
stuff.It takes time and it takes a computer with an internet
connection.But, the tools are free. This is an ideal
scenario.We can do almost anything today.I am very excited to
see the opportunities expand in the last 20 years.
You know what I did not expect to happen so quickly?For
the possibilities to become so good.I did not expect them to
become so refined, so polished in so short a period of time.I
was excited when Java ME came out.Now we have Android and
iOS.I was excited when Hadoop came out.Now we have Spark and
TensorFlow.I was excited when JDBC, the Servlet specification
and Struts came out.Now we have Spring.I was excited when
Netscape introduced JavaScript.Now we have Vue.js and React
and Angular.I was excited when Spring came out.Now we have
Spring Boot.I was excited when C++came out.Now we have
Kotlin.I was excited when Ant came out.Now we have Maven and
Gradle.I was excited when the ideas around continuous
delivery started to crystalize.Now we have a Gitops-centric
culture powered by technologies like Kubernetes.Everything
has gotten so much better in the last 20 years. All of these
things were exciting to me when they first arrived.But
they're better now. They're easier. They're faster. They're
cheaper. They're a natural evolution of the ideas we've known
for a long time.
I feel the same enthusiasm—excitement—when I look at
reactive code.I love Spring.I know that it's one of the most
powerful ways to express my microservices. Today, I am
excited about the opportunity to use Spring and Reactor to
build much more resource-efficient, easier-to-understand,
reactive services.
Reactive Programming is a natural next step in the
creation of cloud native applications.Reactive libraries
offer me several tentpole benefits.
I'll expand on those points here:
Reactive Programming offers one abstraction, no matter
what the application (server-sent events, RSocket, WS, HTTP,
Kafka, etc). A unified abstraction greatly simplifies the
integration and composition of disparate services and data.
Reactive Programming supports more declarative, concise,
deterministic ways to express complex, multithreaded
algorithms.Remember:only one person TRULY understands how to
write safe, concise multithreaded Java code... and it's NOT
you!(It is not me, either!) I don't know who it is.It's
better to let the library and framework do the dangerous work
of managing concurrency.
Reactive Programming supports services that are more
robust.Reactive libraries give us an API protocol to signal
that our consumer is overwhelmed, or that it can not handle
anymore.Libraries like Project Reactor provide operators to
consistently handle errors, back-pressure, and more.The
result is safer code with much fewer lines of code.
I believe that all new projects should embrace Reactive
Programming, if possible.
So, when I saw that there is a book being written in
Chinese to help people understand how to write reactive
applications, I was very excited!I hope you'll take the
opportunity to read this book, dear reader, and to learn how
to work with Reactor and to see how it supports you when
building reactive applications with Spring. The opportunities
we have today are endless.We have the tools to build almost
anything, easily, and to ship that software into production
for very cheap. And I am excited to see what you will build
with these tools.
Josh Long
Spring官方布道师
Java Champion成员
Kotlin GDE谷歌官方认证开发专家
San Francisco, USA
July 2020
推荐序二
Dear Reader,
Welcome on your journey to building more efficient and
resilient applications, welcome to Reactive Programming!
On this path, you will challenge yourself and you will be
rewarded with a new mindset that will help you create your
next distributed Java services.Interacting with remote
services is a common task performed by distributed systems
and those interactions take time outside of the calling
application control.Multiple reasons are behind this time
fluctuation:network latency, the nature of the task run or
even a system interruption. At the end of the day, if the
calling code needs a result back it will be negatively
impacted by that same time because it needs to actively wait
for it.
Enter Reactive Programming which gives you the great
power of bending space-time to your will!Or sort of... It
gives you the ability to remove the negative impact of
waiting for anything, so your Thread or CPU is not pinned and
can perform other tasks.
Since you can't control how long a remote call will last,
you will need to "schedule" the activities your application
needs to perform "when" those remote services produce a
result.For instance, if your application talks to a remote
REST endpoint, it will eventually produce an HTTP response.
"Non Blocking" applications will provide a listener
reacting only"when"an underlying network event signals the
response back to you.What will do the calling Thread then? It
will be able to return to the server worker pool or preparing
a next HTTP call, thus increasing service concurrent
capacity. It's a scalable design some runtimes have adopted
for years, you can implement it with a single thread like
Node.JS!
"Non-Blocking" is foundational to Reactive Programming
but what does "Reactive" mean anyway? It's maybe not the
first time you read the word "Reactive", it has various
meanings and it's even mistaken for the popular UI library
ReactJS. In this book, when "Reactive" is mentioned, it will
always refer to the "reactive-streams" specification which is
a clear documented definition adopted by major libraries.
"Reactive Streams" defines how an arbitrary "producer" can
interact with one or more "consumers" in a Reactive way. Once
implemented, the contract offers two key qualities designed
to help developers build 100% non-blocking applications:
Error Isolation and Flow Control. The former quality
contributes to a greater resiliency and availability,
producers errors must not interrupt the application, and
instead they will forward errors to registered listeners. The
latter quality means that producers can't produce more data
than consumers are able to consume at any given time.This
rule is also known as "backpressure" and it is designed to
avoid "unbounded buffers" which has resources consequences on
the application host. It helps that one of the major adopters
of "Reactive Streams" is the Spring Reactive stack itself
including Spring WebFlux and Project Reactor. They do a great
job at hiding away a lot of the technical details from the
specification I've briefly mentioned in this intro. They
provide rich APIs to build reactive flows and help you focus
on the "what" and not the "how".
In fact, there are various elements in these libraries
designed to ease your first experience with Reactive
Programming.First and foremost, Spring can become as much
reactive as you need:You can start developing on top of a
well-known programming model, Spring MVC, running on Tomcat
and you can selectively introduce the modern reactive
"WebClient" coming with Spring WebFlux.You can also start
returning Project Reactor reactive types Flux and Mono in
your Spring MVC controllers the same way you can return
CompletableFuture.Ultimately you can just swap your container
and start running on top of Netty or Tomcat reactive bridge.
Spring conventions such as annotations also matter and most
Java developers have learned them for sometimes many years!
For instance, @RestController works in both Spring MVC and
WebFlux as one could expect. Learning Reactive Programming
with Spring is intended to feel like you are learning at
home, in a familiar setup.
In this book, you will work with the Spring Reactive
stack. I highly recommend you pair this learning opportunity
with a good use case that could benefit from going Reactive.
For instance, an application depending on a remote service,
or an application on the edge of your system serving as a
port of entry for many concurrent users.
Learning a new mindset is never easy but it is highly
rewarding.Many of the lessons Reactive programming offers
apply to all modern software engineering. I think the
greatest of these lessons is that it always pays off to be
ready with a plan when a component outside your control does
not work as you expect.
Good luck!
Stephane Maldini
Netflix高级软件工程师
Spring Reactor项目创始人
Reactor-Netty项目负责人
Spring Framework项目贡献者
推荐序三
与 本 书 作 者 的 初 识 是 在 2016 年 , 我 那 会 儿 因 为 连 载 一 些 关 于
Spring Boot和Spring Cloud的入门内容,所以组建了一些交流群,于
是跟他便有了交集。而我对他的进一步了解,是在我读了他写的名为
“Spring框架中的设计模式”系列博文后,他对框架原理的研究、理
解与表述都非常优秀,并且这个系列的文章得到了诸多读者的高度评
价!所以我也在我的博客中转载了这个系列的文章,希望更多的
Spring爱好者能够读到。
与作者相识多年,虽说互相交流的时间与次数并不算多,但他一
直以来对技术研究的热情与对技术分享的坚持,是最让我敬佩的。我
本不是一个B站用户,但正是因为他在B站上分享了国外顶级课程与前
沿技术,才让我这个老古董也用上了B站!如果你跟我一样,热爱前沿
技术,追逐先进理念,那么就跟着他的步伐吧,一定可以打开思路并
有所收获!
言归正传,说说作者的“Java编程方法论系列丛书”。如果你是
一名Java开发者,那么你是否听说过响应式编程?如果有,那么有没
有将其应用到日常开发中?我曾经在几个开发交流群中问过这两个问
题,得到的答案大多是:知道但没怎么用过。
这样的状况非常容易理解,每一种新技术、新理念都需要时间去
沉淀与积累,因为有非常多的工作都建立在既有技术框架之上,大象
翻身并不是那么容易的事。新技术与新理念,不会快速地被大规模铺
开应用。但不得不说,这个系列图书为我们带来的内容正是Java生态
在逐步拥抱的编程方法。所以,还有什么理由不去了解和学习呢?
我强烈推荐作者的这个系列图书,因为它们不仅可以帮我们认识
到响应式编程的本质原理,同时也兼顾了我们常用的Spring、Netty、
WebFlux等框架内容,可以很好地帮助我们将理论与实战联系起来,便
于理解与学习。对于这种在国外已经非常流行,而在国内还处于萌芽
状态的编程理念,如果你也跟我们一样认可这个方向,何不跟随着作
者,一起学习它、使用它,为你信仰的技术布道,让身边的技术人都
能为之收益?岂不快哉?
翟永超
《Spring Cloud微服务实战》作者
推荐序四
有幸与本书作者相识多年,他严谨的治学态度和认真的工作作风
都让我佩服不已。
作者是国内响应式编程的布道者,这本新书是他的第二本大作,
千呼万唤始出来,令人激动万分。
作者是一个非常乐于分享知识和为别人解答困惑的人。这本关于
响应式Reactor的著作是他反复校对并认真打磨的精华之作。写书殊为
不易,分享精神更是难能可贵,他能把自己积累的Reactor技术经验总
结成Java编程方法论并出版成书是一件非常了不起的事情。因此,当
我得知他要将自己对Java编程和响应式编程的技术经验编写成书的时
候,甚是欣喜。因为我相信这本书不仅会让读者对响应式编程等核心
知识有深入理解和技术成长,同时也会让读者对Java编程方法论有自
我思考。
响应式编程是未来架构的一次升级。如果你有幸阅读过作者的第
一本书《Java编程方法论:响应式RxJava与代码设计实战》,那么这
本书非常适合作为你的进阶读物。在Java社区中,RxJava和RxJava 2
非常流行,而另一个新的响应式编程库就是Reactor。Reactor是完全
基于响应式规范设计和实现的库,在使用上直观、易懂,而且Reactor
也是Spring 5中响应式编程的基础。因此,学习和掌握Reactor可以让
我们更好地在Spring 5中使用WebFlux。
对于我们而言,响应式编程是思维方式的升级,也是一次充满更
多可能性的机会。随着响应式编程技术的成熟,如果能把全异步流式
能力引入业务开发中,不仅可以帮助项目提升性能,还可以给项目带
来更多的灵活性。不论是刚接触响应式编程的入门读者,还是已经有
响应式编程实战经验的进阶读者,阅读这本书都会收获颇多。
梁桂钊
《高可用可伸缩微服务架构》联合作者
公众号“服务端思维”作者
推荐序五
自从Java第一框架Spring Framework 5.0(2016年6月发布了第一
个里程碑版本)内置响应式组件(spring-webflux)开始,响应式编
程逐步进入了广大程序员的视野。响应式编程经常被理解成并发编程
或者高性能编程,因为它们看起来很相似,以至于很多人无法分清这
些概念,但是实际上设计原则完全不同,这不可避免地给人带来了一
些困惑。人们常把响应式编程和函数式响应式编程(FRP)混为一谈。
一些人觉得响应式编程换汤不换药,他们早就在这么写代码了,但其
实并不一样。在Java企业级应用开发领域,也有一些关于响应式编程
的探索,虽然取得了一些不错的成果,但也暴露出很多容易犯的错
误。
响应式编程是从命令式编程到声明式异步编程的重大转变,是企
业级应用架构的一次重大升级。要想正确地编写出优质、高效的响应
式代码,需要在编程思想上发生重大的转变,就如同20年前从面向过
程编程到面向对象编程的转变一样。
知秋在响应式编程方面的认知及落地能力,得到了国内业界人士
的广泛认可,他出品的教学视频也得到了国外专家们的肯定及推广,
他是国内名副其实的响应式编程和NIO领域的专家。这本书的出现可谓
是及时雨,对有相关学习、工作需求的小伙伴来说,是很好的指导。
但要注意的是,该“Java编程方法论系列丛书”不是泛泛的API讲解,
也不是快速入门指南,更不是玄而又玄的概念堆叠,而是成体系的、
传授编程思想和技巧的响应式编程学习图书。如果你内心不够强大,
那么这本书或许不适合你,因为阅读它并不轻松,但当你坚持阅读数
小时并收获知识时,幸运的你将在技术上得到成长。
总有那么几本书,它们会影响我们的思维习惯,甚至改变我们看
待这个世界的方式。我真心地希望能早几年看到这本书,因为我确信
它会给读者带来很有意义的影响。
于文龙
国药控股上海生物医药有限公司 架构师
推荐序六
随着Project Reactor 3(后来的Spring Reactor 3)在Spring
Framework 5.2中正式登场,响应式编程“杀入”了国内应用开发的前
线。不少开发团队为了能够实现低延迟和高吞吐量而选择使用Spring
WebFlux。但这并非易事,全新的编程风格和设计方式让很多程序员望
而却步。相信国内不少对响应式编程有一些认识和了解的人都看过本
书作者分享的内容,他被视为国内少有的响应式编程和NIO方面的专
家 , 多 年 来 一 直 坚 持 对 JDK 、 Reactor 、 Reactor-Netty 、 Spring
WebFlux和RxJava等技术进行源码解读和知识分享。时至今日,Spring
全家桶和Project Reactor的子项目依旧保持着活跃的更新频率,作者
也一直保持着对这些项目的代码设计和源码更新的关注。为了帮助国
人更快地接受响应式编程思想,近两年他分享了大量相关前沿技术、
源码解读和设计思路的视频和文章,并将这些内容发布到B站
( https://space.bilibili.com/2494318 ) 和
simtoco(https://www.simtoco.com)上,供需要的人观看和学习,
这些视频无疑是国内响应式编程领域的宝贵财富。与此同时,“Java
编程方法论系列丛书”的第二本书伴随着这个过程,历经反复迭代,
终于出版了!
以往,由于Java程序中存在着过多或过重的线程及I/O阻塞,因此
系统性能浪费情况严重。如果将它们替换为完全异步的处理方式,就
能够让机器发挥出更优秀的性能,减少不必要的浪费。当下,Spring
的相关项目都在向响应式编程的方向发展,由Spring所提供的Reactor
为Java程序员带来了更高性能的编程实现方式。但想要驾驭Reactor并
不容易,如果你对Reactor理解得不够,错误的使用方式将会导致你的
响应式程序的性能不及传统的命令式程序,因此对Reactor的认识和理
解非常重要。限于篇幅,这本书无法将Spring Reactor的一切都一一
展现出来,作者在网络上更新发布的相关分享视频同样值得学习。图
书较为系统,而视频则更加灵活,可以看作这本书的补充和拓展。
响应式编程基础库Reactor在Java编程中正变得无处不在,就像
Netty被用在众多涉及网络通信的开源项目中一样,Reactor也必将出
现在更多流行的开源项目中,诸如Reactor-Netty和Spring WebFlux,
而这些项目必将成为网络通信、IoT及Web等应用领域中的新主流。能
够越早地对Reactor的设计思想与实现方式有所认识和了解,你在未来
的工作和学习中将越早地把握住自己的未来。
尹相宇
simviso成员
推荐序七
响应式编程的概念最早是在20世纪90年代末提出的,其让微软的
Erik Mejier从中获取了灵感,设计、开发了.NET的Rx库(2012年开
源),这也是响应式编程最初的实现。在此之后,Reactive Streams
出现了。最开始,它是由NetFlix、Pivotal(现为Vmware Tanzu)及
LightBend等几家公司的开发人员提出的。紧接着,在2015年,JDK正
式将Reactive Streams作为标准纳入,其就是我们熟知的JDK 9中的
Flow API。这也从侧面证明了响应式编程是多么优秀,连JDK都将其收
入麾下。
随着近些年响应式编程理念的兴起,越来越多的厂商逐步将其投
入使用。NetFlix和Spring是其中的佼佼者。为了进行响应式改造,
Spring将Project Reactor纳入旗下进行孵化,而Project Reactor后
来成了我们熟知的Spring Reactor。在Web方面,正如Josh Long所说
的,随着并发量不断增加及微服务架构流行,传统的Spring MVC渐渐
无法满足我们的需求,这也让我们感受到了传统I/O的局限性。虽然我
们可以通过增加线程来提高性能,但这并不是最佳解决方案。线程对
于我们来说可能是廉价的,但是对于JVM之类的平台来说,则是一种很
宝贵的资源。因此,基于Spring Reactor的Spring WebFlux也就应运
而生了。正如Spring官方文档所讲的那样,响应式编程虽然不能让程
序跑得更快,但它所具备的这种异步、非阻塞的特性能够让程序以较
少的固定数量线程和较少的内存来处理更多的业务。这样能够充分利
用机器资源,从而避免了我们以往为了提高性能而不停地增加机器的
尴尬局面。
响应式编程是未来的趋势,对于Java开发人员而言,它的到来无
疑使整个Java生态体系得到了一次升华,并且改变了我们以往的思维
方式及开发方式,也改变了众多开发人员以往对Java的看法。可以
说,响应式编程给Java带来了第二春。
目前,响应式编程在国内才刚刚开始,知秋所编写的“Java编程
方法论系列丛书”无疑填补了国内这个领域的空白。如果你对响应式
编程了解不多,可以先阅读这本书的前作《Java编程方法论:响应式
RxJava与代码设计实战》。为了能让读者更好地入门并掌握响应式编
程,知秋在B站和simtoco上录制并上传了大量相关源码解读视频,感
兴趣的小伙伴可以关注B站上的simviso官方页面,以及simtoco官网。
在阅读这本书时,建议你一定要反复阅读,注意细节。如果你的Java
基础不是很牢固,那么切记不要随意跳读,你可以跟随配套解读视频
进行学习,这样能降低学习难度。
最后,我相信响应式编程必将在未来大放光彩。
刘嘉诚(花名 虚生花)
simviso成员
前言
最近几年,随着Go、Node等新语言、新技术的出现,Java作为服
务器端开发语言老大的地位受到了不小的挑战。虽然Java的市场地位
在短时间内并不会发生改变,但Java社区还是将挑战视为机遇,并努
力、不断地提高自身应对高并发服务器端开发场景的能力。
为了应对高并发服务器端开发场景,在2009年,微软提出了一个
更优雅地实现异步编程的方式——Reactive Programming,我们称之
为响应式编程。随后,各语言很快跟进,都拥有了属于自己的响应式
编程实现。比如,JavaScript语言就在ES6中通过Promise机制引入了
类似的异步编程方式。同时,Java社区也在快速发展,Netflix和
LightBend公司提供了RxJava和Akka Stream等技术,使得Java平台也
有了能够实现响应式编程的框架。
当下,我们通过Mina和Netty这样的NIO框架其实就能完成高并发
下的服务器端开发任务,但这样的技术只掌握在少数高级开发人员手
中,因为它们难度较大,并不适合大部分普通开发者。
虽然目前已经有不少公司在实践响应式编程,但整体来说,其应
用范围依旧不大。出现这种情况的原因在于当下缺少简单、易用的技
术,这些技术需要能使响应式编程更加普及,并做到如同Spring MVC
一样结合Spring提供的服务对各种技术进行整合。
在2017年9月28日,Spring 5正式发布。Spring 5发布最大的意义
在于,它将响应式编程技术的普及向前推进了一大步。而同时,作为
在背后支持Spring 5响应式编程的框架Spring Reactor,也进入了里
程碑式的3.1.0版本。
在本书中,我会带着大家学习响应式编程,并通过逐层递进的方
式对Spring Reactor的源码设计与实现进行解读,以揭示其中的设计
精髓,帮助大家灵活运用及提升代码设计思维。
限于篇幅,本书不可能涉及Spring Reactor的所有知识点。作为
本书的有效补充,我特意录制了一套针对Spring Reactor源码进行全
面解读的配套视频。未来,我也会根据Spring Reactor版本的更新迭
代,适时地推出新的解读视频,帮助大家走在响应式技术发展的最前
沿。
最后,感谢Spring官方布道师Josh Long和Spring Reactor项目创
始人Stephane Maldini在百忙之中为本书作序。作为响应式编程研究
人员,受到官方认可,深感荣幸!另外,也要感谢家人及simviso小伙
伴的一路支持。
该套视频的地址与本书的配套源码一起放在同一个GitHub代码仓
库中,地址如下:
https://github.com/muyinchen/Java-programming-
methodology-Reactor-articles
读者服务

微信扫码:
· 获取博文视点学院在线课程、电子书20元代金券
· 获取本书配套视频及源码资源
· 获取国外知名Java开发者分享视频(中文字幕)
· 加入本书读者交流群,与更多读者互动
第1章 响应式编程概述
响应式编程到底是什么?在现实生活中,当我们听到有人喊我们
名字的时候,会对其进行响应,也就是说,我们是基于事件驱动模式
来进行编程的。所以这个过程其实就是下发产生的事件,然后我们作
为消费者对下发事件进行一系列的消费。
从这个角度来说,对整个代码的设计应该是针对消费者来进行
的。比如,看电影,有些画面我们不想看,那就闭上眼睛;有些声音
不想听,那就捂上耳朵。其实这就是对消费者的增强包装,我们把复
杂的逻辑拆分开,然后将其分割成一个个小任务进行封装,于是就有
了诸如filter、map、skip、limit等操作。本书会用大量的篇幅来解
读源码设计逻辑。

1.1 并发与并行的关系
可以说,并发很好地利用了CPU时间片的特性,也就是操作系统选
择并运行一个任务,接着在下一个时间片内运行另一个任务,并把前
一个任务设置成等待状态。其实并发并不意味着并行。
具体列举下面几种情况。
◎ 有时候,多线程执行会提高应用程序的性能,而有时候反而会
降低应用程序的性能。这在JDK中Stream API的使用上体现得
很明显,如果任务量很小,而我们又使用了并行流,反而降
低了应用程序的性能。
◎ 在多线程编程中,可能会同时开启或者关闭多个线程,这样会
产生很大的性能开销,也降低了应用程序的性能。
◎ 当线程同时处于等待I/O的过程中时,并发可能会阻塞CPU资
源,其后果不仅是用户长时间等待,而且会浪费CPU的计算资
源。
◎ 如果几个线程共享了一个数据,情况就会变得有些复杂。我们
需要考虑数据在各个线程中的状态是否一致。为了达到数据
一 致 的 目 的 , 很 可 能 会 使 用 synchronized 或 者 lock 相 关 操
作。
现在,你对并发有一定的了解了吧。并发很好,但并不一定会实
现并行。并行是在多核CPU上同一时间运行多个任务或者一个任务分为
多块同时执行(如ForkJoin)。单核CPU的话,就不要考虑并行了。
补充一点,实际上多线程就意味着并发,但是并行只发生在这些
线程在同一时间调度、分配到不同CPU上执行的情况下。也就是说,并
行是并发的一种特定形式。一个任务里往往会产生很多元素,这些元
素在不参与操作的情况下大都只能处于当前线程中,这时我们可以对
其进行ForkJoin,但这对很多程序员来讲有时候很不好操作、控制,
上手难度有些大。这时如果用响应式编程,就可以简单地通过所提供
的调度API轻松做到事件元素的下发、分配,其内部会将每个元素包装
成一个任务并提交到线程池中,我们可以根据任务是计算型的还是I/O
型的来选择相应的线程池。
在这里,需要强调一下,线程只是一个对象,不要把它想象成CPU
中的某一个执行核心,这是很多人都在犯的错,CPU时间片会切换执行
这些线程。

1.2 如何理解响应式编程中的背压
背压,由Back Pressure翻译得到,从英文字面意思讲,称之为回
压可能更合适。首先解释一下回压,它就好比用吸管喝饮料,将吸管
内的气体吸掉,吸管内形成低压,进而形成饮料至吸管方向的吸力,
此吸力将饮料吸进人嘴里。我们常说人往高处走,水往低处流,水之
所以会出现这种现象,其实是重力所致。而现在吸管下方的水上升进
入人的口中,说明出现了下游指向上游的逆向压力,而且这个逆向压
力大于重力,可以称这种情况为背压。这是一个很直观的词,向后
的、往回的压力——Back Pressure。
放在程序中,也就是在数据流从上游源生产者向下游消费者传输
的过程中,若上游源生产速度大于下游消费者消费速度,那么可以将
下游想象成一个容器,它处理不了这些数据,然后数据就会从容器中
溢出,也就出现了类似于吸管例子中的情况。现在,我们要做的事情
就是为这个场景提供解决方案,该解决方案被称为背压机制。
为了更好地解决背压带来的问题,我们回到现实中看一个事物
——大坝。在发洪水期间,下游没办法一下子消耗那么多水,大坝此
时的作用就是拦截洪水,并根据下游的消耗情况酌情排放,也就是
说,背压机制应该放在连接元素生产者和消费者的地方,即它是生产
者和消费者的衔接者。然后,根据上面对大坝的描述,背压机制应该
具有承载元素的能力,也就是它必须是一个容器,而且其存储与下发
的元素应该有先后顺序,那么这里使用队列是最适合的了。背压机制
仅起承载作用是不够的,正因为上游进行了承压,所以下游可以按需
请求元素,也可以在中间根据实际情况进行限流,以此上下游共同实
现了背压机制。在本书后续内容及相关的配套视频中会介绍背压的相
关API。

1.3 源码接口设计启示
关 于 响 应 式 编 程 的 Rx 标 准 , 已 经 写 入 了 JDK 中 , 即
java.util.concurrent.Flow:
可 以 看 到 , Flow 类 中 包 含 了 4 个 接 口 定 义 , Publisher 通 过
subscribe 方 法 与 Subscriber 产 生 订 阅 关 系 , 而 Subscriber 依 靠
onSubscribe与上游源生产者产生联系,这里是通过Subscription做到
的,所以Subscription往往会作为生产者的内部类进行定义,用来接
收生产者所生产的元素,如果其支持背压,那么就和大坝蓄水一样。
Subscription首先应该将元素放入一个队列中,然后根据元素请求数
量来调用Subscriber的onNext等方法进行元素的下发。该实现方法在
响 应 式 编 程 中 都 是 统 一 的 模 式 , 下 面 通 过 Reactor 中
reactor.core.publisher.Flux#fromArray所涉及的FluxArray的源码
来进行理解:
大家可以结合前面的内容与源码内部的注释来理解源码细节。对
于各种中间操作的包装,我们该如何做呢?依据之前的接口定义,我
们应该更注重功能的设定。filter、flatMap、map等常用操作其实都
是消费动作,理应定义在消费者层面,想到这里,我们又该如何做
呢?
在 这 里 , 我 们 要 结 合 设 计 模 式 —— 装 饰 器 模 式 , 对
subscribe ( Subscriber< ? super T>subscriber ) 所 传 入 的
Subscriber进行功能增强,即从Subscriber这个角度讲,使用的是装
饰 增 强 模 式 。 但 从 外 面 来 看 , 其 整 体 定 义 的 依 然 是 一 个 Flux 或 者
Mono。FluxArray就是这样一个例子,从生产源角度来说,它通过继承
Flux<T>来对外表示自己的生产源身份;而从功能增强处理的角度来
说,其依靠的是内部类对所传入的Subscriber进行包装增强。最后通
过subscribe(Subscriber<?super T>subscriber)将生产源与订阅
者衔接起来,完成一整套标准流程的设定。
可 以 结 合 reactor.core.publisher.Flux # filter 涉 及 的
FluxFilter来观察和理解上述内容:
根据这些设计,完全可以以这套API接口设计作为参考,衍生出很
多规范逻辑的开发实现,比如众多的Rx衍生操作API的设计实现,它们
都是按照一套模板来实现的,可以称之为代码层面的微服务设计模
板。

1.4 如何看待众多函数表达式
人类最擅长描述场景,比如一套动作,这套动作放在舞蹈层面,
可以是名为×××的编舞,但是这个编舞又要处于一定的框架之下,
即需要一定的规范。同样,我们施展一套拳法,也需要一个规范,不
能踢一脚就叫拳法。而规范的实现,可能就是几个很简单的左勾拳或
者右勾拳组合,也可能是比较复杂的咏春拳、太极拳等,而且一套拳
法可能由很多小套路组成,这些小套路也都遵循着规范。那么按照这
个思路,我们来看看下面的函数式接口的定义:
可以看到,无论是条件判断表达式Predicate还是无返回值动作处
理函数BiConsumer,都遵循一个标准动作的设计定义思路,并通过
default方法来对同类动作进行编排,以达到更加丰富的效果。所以,
函数式的应用更倾向于干净利落,凸显自己要做的事情,而基于这些
动作进行的一整套流程设计,由于我们无须关心具体某个函数式接口
如何实现,因此完全可以这么说,函数式设计开发是面向未知实现开
发的一种方式,而响应式编程很好地结合了这种编程思维。

1.5 Reactor与RxJava的对比
关于响应式编程,我写的《Java编程方法论:响应式RxJava与代
码设计实战》一书已经出版,那么Reactor与RxJava又有什么区别呢?
首先我要明确地告诉你,如果你使用的是Java 8+,那么推荐使用
Reactor 3,而如果你使用的还是Java 6+或函数需要做异常检查,那
么推荐使用RxJava 2。
下面来看图1-1。
从图1-1可以看到,RxJava 2和Reactor共用了一套接口API标准
Reactive Streams Commons,这也说明它们的最终目的是一致的,而
且API具有通用性,这样也降低了学习成本。
下面再来回顾一下RxJava。迄今为止,RxJava发行版主要分三大
版本RxJava 3、RxJava 2和RxJava 1。与RxJava 1不同,RxJava 3、
RxJava 2直接通过新添加的Flowable类型来实现Publisher的接口定义
(RxJava 3与RxJava 2并没有太多区别,故这里只介绍RxJava 2)。
同时,RxJava 2依然保留了RxJava 1中的Observable、Completable和
Single,并引入了支持Optional的Single升级版——Maybe类型(这点
在《Java编程方法论:响应式RxJava与代码设计实战》一书中并没有
提及,此处只是说明一下,并不会深究)。RxJava 1中的Observable
不支持RxJava 2中的背压机制,背压机制是Flowable的专有功能,不
过Observable内部提供了可转换API。需要注意的是,Observable实现
的是RxJava 2中自定义的ObservableSource接口。

图1-1
在Reactor中,可以发现Mono和Flux两种类型都实现了Publisher
接 口 , 同 时 两 者 皆 实 现 了 背 压 机 制 。 Flux 可 以 对 标 RxJava 2 中 的
Flowable类型,而Mono可以被理解为RxJava 2中对Single的背压加强
版。后续,我们会进行更深入的讲解。
同样,下面再来了解一下Reactor与RxJava的不同之处。
◎ 为了兼容Java 1.6+,RxJava不得不自行定义了一些函数式接
口 , 可 以 参 考 io.reactivex.functions 下 的 接 口 定 义 。 而
Reactor 3则是基于JDK中提供的java.util.function来设计
实现的。
◎ 可以很轻松地从java.util.stream.Stream转换为Flux,也可
以很轻松地由后者转换为前者。
◎ 同样,可以很轻松地实现CompletableFuture与Mono之间的互
相转换,也可以轻松而安全地基于Optional类型的元素创建
Mono。
◎ 从图1-1中还可以看到,Reactor 3可以更好地服务于Spring
Framework 5,也更适应最新版本的JDK。
最后,简单介绍一下图1-1中的几个部分。
◎ Core:是我们主要研究的库,是Reactor的核心实现库。其作
用与RxJava 2的核心实现的作用是一样的,本书主要介绍
reactor-core模块。
◎ IPC:可以认为它是针对encode、decode、send(unicast、
multicast或request/response)及服务连接而设计的支持背
压的组件。IPC支持Kafka、Netty及Aeron。
◎ Addons : 其 中 包 括 reactor-adapter 、 reactor-logback 和
reactor-extra。reactor-adapter可以说是连接RxJava 1/2
中Observable、Completable、Flowable、Single、Maybe、
Scheduler的桥梁,可以方便地与Reactor 3进行转换操作。
同样,这个库对于Swing/SWT Scheduler、Akka Scheduler也
做了针对性适配。reactor-logback用于支持Reactor Core异
步 处 理 Logback 方 面 的 功 能 。 reactor-extra 为 数 字 类 型 的
Flux源提供了很多数学运算的操作。
◎ Reactive Streams Commons:是RxJava 2和Reactor共用的一
套接口API标准。

1.6 小结
经过本章的介绍,大家对响应式编程的相关概念有了一个初步的
认识,同时通过一张图对比了RxJava和Reactor的区别。接下来,会开
始 探 索 Spring Reactor 核 心 实 现 库 reactor-core , 本 书 主 要 基 于
Reactor 3.1.x版本进行讲解。
第2章 对Flux的探索
N
Flux是可以发出0到 个元素的生产者。它实现了Publisher<T>接
口,而此接口的核心方法是public void subscribe(Subscriber<?
super T>s),下面就来探索一下它的源码细节。
首先看看相关的实现效果图,如图2-1所示。

图2-1
由图2-1可知,在产生订阅后,主要执行以下这几个逻辑。
◎ 首先,在产生订阅后,会调用生产源Flux的subscribe方法,
接着调用订阅者Subscriber的onSubscribe方法,此时执行的
是我们针对Subscription定义的一些代码逻辑Consumer<?
super Subscription>,即图中三角形所代表的代码逻辑。该
代码逻辑主要用于调用Subscription#request方法,结合前
面对背压机制的介绍,可以知道,Flux内部进行了背压设
计。
◎ 接着执行我们定义的onNext方法,即图中圆形所代表的代码逻
辑。
◎ 如果生产源的所有元素都能够正常下发完毕,则调用我们定义
的onComplete方法,即图中方块I所代表的代码逻辑。
◎ 如果生产源下发的元素在消费过程中产生了异常,则调用我们
定义的onError方法,即图中方块X所代表的代码逻辑。
对于具体细节,将在下面的章节中进行梳理。

2.1 对Flux.subscribe订阅逻辑的解读
下面直接看看Flux.subscribe的订阅逻辑相关源码,对其进行一
一分析,从而引出相关内容:
在 这 里 , Consumer 参 数 类 型 使 用 了
java.util.function.Consumer , 是 从 JDK 8 开 始 使 用 的 。 先 跳 过
LambdaSubscriber的定义,通过onLastAssembly方法可以知道,我们
提前自定义了一个钩子函数Hooks.onLastOperatorHook,这样在每次
发生订阅时都会进行统一的动作操作,可以认为这是一个拦截器。其
典型应用是测试时的应用,在本书第10章中,将会看到Reactor 3测试
库的编写实现,其中会涉及onLastAssembly方法的使用细节。
2.1.1 对CoreSubscriber的解读
从 上 面 的 源 码 可 以 发 现 , LambdaSubscriber 实 现 了
CoreSubscriber接口。根据该接口,可以衍生出各种各样的订阅者,
对于生产者的Publisher接口,也是如此,所以从上面源码最后一行可
以看到,Flux留了一个抽象的subscribe方法,留给具体的实现类来实
现 。 只 是 最 初 的 Publisher # subscribe ( Subscriber ) 的 参 数 类 型
Subscriber 变 为 了 CoreSubscriber , 而 为 了 保 证 Rx 标 准 的 统 一 ,
CoreSubscriber继承了org.reactivestreams.Subscriber接口,同时
加入了一些Reactor 3特有的Context功能实现。
下面对CoreSubscriber来进行分析:
从这个接口的注释可以知道,如果订阅者发出的元素请求数量小
于或等于0,则请求不会产生onError事件,而只会简单地忽略错误。
我们可以将其与RxJava 2中的实现进行对比:

可以看到,当元素请求数量n小于或等于0时,会产生一个onError
事件,并标明此事件为参数异常事件类型。Reactor 3.1+中的实现方
式如下:
从上面的实现源码可以看出,这里仅记录了日志,并没有产生
onError事件。
另外,从currentContext的定义可知,其主要用于元素下发过程
中的中间操作或者中间定义的订阅者上。内部涉及的Context,主要用
于存储此订阅者产生订阅到结束这一过程中的信息(比如异常信息、
临时中间变量),这些信息可以被订阅者获取,其有点类似于
ThreadLocal,但它是针对多线程调度下Reactor特有的东西,后面会
用专门的篇幅来进行介绍。知道了这些内容,再回到当前环境下,在
这里,根据具体的错误操作或者丢弃操作来做一些具体的设置,比
如:

上述源码涉及两块内容:
其 一 , 如 果 已 经 结 束 下 发 , 那 么 采 用 放 弃 策 略
Operators.onNextDropped。
其二,如果往队列中添加元素失败,那么针对这个异常包装出一
个 错 误 事 件 Operators.onOperatorError , 用 于 下 发 错 误 ( 可 以 在
FluxPublishOn.PublishOnConditionalSubscriber # checkTerminated
中 看 到 该 过 程 ) , Operators.onNextDropped 和
Operators.onOperatorError 的 内 部 都 使 用 了
actual.currentContext。此处,只分析Operators.onNextDropped,
剩下的内容请读者自行探索:

在 默 认 的 情 况 下 , 由 actual.currentContext 可 知 , 传 入 的
context会产生一个Context0实例,这里的0代表其中没有键值对,而
假如这个数是1,则代表有一个键值对,这个Context0实例是不可变
的,调用该Context0实例的put方法,也就是重新生成一个全新的实
例,这样也就保证了整个操作上下文的安全(因为原来那个对象并未
发生改变,也就是那些管理着原来那个对象的线程根本无须担心对象
会发生改变)。相关源码大家可以自行查阅,此处点到为止。
在这里,如果默认情况下不存在Hooks.KEYONNEXT_DROPPED这个
key,它会返回一个null,这时会将Hooks.onNextDroppedHook赋值给
hook,并在下一个if判断中调用执行该hook。假如Reactor全局环境下
并没有设定Hooks.onNextDroppedHook这个钩子函数的实现,而且此时
开 启 了 Debug 日 志 管 理 , 则 进 入 log.debug , 同 样 也 可 以 自 行 设 定
actual.currentContext,这样也就做到了自定义CoreSubscriber所特
有的异常处理机制。
以上就是Reactor 3.1+中对异常的一些处理,未来在我们开发拓
展API的时候,可以适当地使用这些处理方法。
2.1.2 对LambdaSubscriber的解读
在产生订阅时往往会自定义一些元素消费操作,这些操作会被
Reactor 3包装成一个LambdaSubscriber类型的实例。这个类中有一些
值得我们学习的亮点:
从传统的订阅逻辑来看,首先会调用onSubscribe方法,如果没有
定义subscriptionConsumer,默认会最大化元素请求数量。在消费下
发元素的时候调用onNext方法,其中的代码逻辑比较简单,不再赘
述。
接下来,将要介绍的是LambdaSubscriber中的一种很实用的使用
原子类的方法,即AtomicXxxFieldUpdater的技法应用,该技法可以直
接应用于实际的项目。
2.1.3 AtomicXxxFieldUpdater的技法应用
在《Java编程方法论:响应式RxJava与代码设计实战》一书中,
提及的RxJava 2的源码中也大量使用了原子类的一些特性用法,但它
们往往是基于类级别的操作,这就导致其相对不灵活。假如一个类中
需要定义两个或更多类型的原子类,仅仅将类本身定义为原子类来进
行操作是完全不够的。那么有没有一个既可以基于类本身又可以与多
个原子类相关的方便操作呢?本节就来展示这种技法。
首 先 , 定 义 一 个 volatile 变 量 : volatile Subscription
subscription。
然 后 , 因 为 此 变 量 是 一 个 Subscription 类 型 对 象 , 所 以 通 过
AtomicReferenceFieldUpdater.new
Updater ( LambdaSubscriber.class ,
Subscription.class , "subscription" ) 将 其 加 入 原 子 类 管 理 字 段
中 , 也 就 是 LambdaSubscriber.class 类 下 的 一 个 类 型 为
Subscription.class 的 volatile 变 量 字 段 , 其 字 段 名 称 为
subscription,并得到一个AtomicReferenceFieldUpdater类型的变量
S。
最 后 , 通 过 S.getAndSet ( this ,
Operators.cancelledSubscription())将此subscription变量的值
通过原子类操作进行改变。此操作返回的是修改subscription之后的
值。
这其中到底发生了什么,下面试着探讨一下相关源码:
从newUpdater方法的注释可以看出,在3个参数中,tclass为所操
作的目标类的类型,vclass为所操作目标类中字段的类型,fieldName
为 所 操 作 目 标 字 段 的 名 字 。 这 个 方 法 返 回 一 个
AtomicReferenceFieldUpdaterImpl类型实例。
而在AtomicReferenceFieldUpdaterImpl构造器内,进行了一系列
反射操作,以及一些对类和字段的权限判定和异常判断。请注意,这
里的this.offset的初始值就是我们所传递字段在对象分配地址中的相
对位置(可以这么理解:我摸到你的头顶,然后鼻子距离头顶的
offset是10cm的话,我就很容易通过相对距离找到鼻子),这个相对
位置在内存加载class字节码的时候就已经确定了。另外,所传递的目
标字段必须是引用类型的,而且目标字段必须是volatile变量。CAS就
是针对volatile的特性来做的基于内存级别的更加直接的优化操作,
我们在实际应用的时候,切记将目标字段定义为volatile变量是一个
大前提。其在JDK中的应用场景非常多,比如Varhandler操作(基于
JDK 9+)。所以,此处的代码依然是可以进行迭代的,迭代方式请参
考 AQS 的 设 计
(java.util.concurrent.locks.AbstractQueuedSynchronizer):
通过AQS的设计可以发现,好的代码库也是把JDK源码中的一些设
计拿来用而已,而且我们也可以找到迭代权威代码库的切入点,不断
提升自己的水平。
接 着 来 看 看 S.getAndSet ( this ,
Operators.cancelledSubscription()),下面是它的源码:
此处,根据类生成的对象分配地址和此地址的偏移量offset,找
到此对象在JVM中存储的定义字段的索引,获取这个字段对应的值,然
后 与 新 值 一 起 执 行 CAS 操 作 。 这 么 说 可 能 有 些 抽 象 , 下 面 来 看 看
this.offset=U.objectFieldOffset(field)的相关源码:
从这段源码的注释可以知道,不要指望对内存地址的相对偏移量
执行任何改变操作,另外,一个对象中的两个字段的偏移量是不可能
相同的。
这也是原子类操作的核心概念,但是很多图书和博客都没把这点
讲清楚。由此可见,Reactor也应用了JDK 8中的一些东西。

2.2 用Flux.create创建源
讲解完订阅操作的细节,下面来介绍一下Flux.create源的创建。
而且,还可以将其与RxJava 2的Flowable.create进行对比。下面先来
看看Flux.create的源码:
从上面的源码可以看到,需要传入的参数emitter和背压策略,以
及 与 RxJava 2 不 一 样 的 创 建 支 持 源 的 模 式 。 这 里 要 注 意 的 是 ,
Consumer方法接收的参数类型为?super FluxSink<T>,这是在告诉我
们它需要一个FluxSink(这是一个单接口)消费类型。此处,我们不
对<?extends T>和<?super T>的具体区别进行分析,感兴趣的读者
可以在网上查阅相关资料。但这里要说明一点,那就是可以直接将<?
super T>里的问号看成Object。假如操作的是一个容器的话(List<?
super T>),可以很轻易地添加T类型的父类或子类型(默认类型向上
强转)的对象,并将其全部看作Object。但List<?extends T>则不
行,如果T类型的子类是T1、T2,而T1、T2根本就不是一个类型的(添
加的时候并不会默认类型向上强转),这时编译器将无法确定具体是
哪个类型,因为编译器确定的是问号所属的类型,而不是后面的上限
或者下限,编译器只进行检查。这点需要记住!所以,在给List<?
extends T>添加T1、T2类型的元素时,会出错。
问号所表达的意思是,我就是这个T类型的子类(extends,所以
不可能会默认强转类型)或者父类(super,自己可以是自己的父
类),这也是很多人一直迷惑而久久不能熟练使用的原因。
FluxSink拥有与RxJava 2中的FlowableEmitter相似的能力,只不
过 Reactor 更 加 直 接 。 在 RxJava 2 中 可 以 看 到 Flowable
create ( FlowableOnSubscribe source , BackpressureStrategy
mode),其中的FlowableOnSubscribe虽然有一个void subscribe(@
NonNull FlowableEmitter<T>e)方法,但是直接将其看作JDK 8+中的
一个java.util.function.Consumer#accept方法即可,accept方法也
接收的是T类型,返回void,所以Reactor 3直接省掉了诸多麻烦的包
装,直接调用java.util.function.Consumer轻装上阵,这时业务也更
加清晰明了了。从业务层面来看,FlowableEmitter表达的就是一个产
生数据源的动作。
2.2.1 FluxCreate细节探索
接下来,看看FluxCreate的一些源码细节:
从subscribe方法来看,其首先做的是根据背压策略包装一个用于
做元素下发动作的sink类。我们知道,只有在产生订阅的时候(即调
用subscribe方法的时候),才会进行元素的生产、下发。可以将这个
subscribe方法看作执行一条业务链的触发者。对于这条业务链中每个
环节的任务,我们往往只能确定要做什么事,会得到什么类型的结
果,但暂时不会去考虑该环节任务的具体实现。我们可以将这些任务
看作抽象任务,而这些抽象任务则是基于subscribe方法所传入的
CoreSubscriber(订阅者)来进行装饰增强包装并承载的。但从上游
源生产者的角度看,它所操作的就是一个很单纯的CoreSubscriber对
象,只需要基于CoreSubscriber的几个核心方法来进行设计实现即
可。
我们来看看用于承接上游源生产者与下游订阅者之间联系的
BaseSink的相关源码:
结合之前介绍的内容,对于上面源码BaseSink中定义的volatile
变量的一些操作,我们理解起来应该完全没有压力,包括
request(long n)内部的细节,难点主要在原子操作上,前面已经讲
解过了。关于request(long n),它存在于Reactor 3库中的很多地
方,其实现的整体思路与RxJava 2中的思路没太多差别,仅request请
求元素的存储方式发生了些许改变。
再次强调,在产生订阅的时候,需要先确定元素请求数量,即订
阅者会先调用onSubscribe方法来确定数量,并在此onSubscribe方法
中 调 用 FluxCreate.BaseSink # request 方 法 , 在 其 最 后 调 用
onRequestedFromDownstream,即根据不同的策略来执行相应的元素下
发动作(支持背压的话,会调用BufferAsyncSink内的drain方法);
接着接入生产元素的逻辑Consumer<?super FluxSink<T>>source,进
行从生产者到消费者的对接,也就是下面源码中的代码逻辑:
接下来讲解一下具体策略的实现,其整体思路和RxJava 2中的是
一样的。拿BufferAsyncSink来讲,同样是队列、drain操作,对于
Reactor 3来说,其buffer-size是256,产生源的队列依然是一个无界
队列,只不过初始大小为256:

我们来看一个Demo:
其默认的背压策略为OverflowStrategy.BUFFER,然后测试会产生
如下结果:
在这里,publishOn操作实现了小货车的功能,在RxJava 2中也有
类似的功能,具体的区别后面详解。可以看到,整个执行效果与
《Java编程方法论:响应式RxJava与代码设计实战》一书中介绍过的
RxJava 2中的执行过程没什么区别。关于其他背压策略,就不赘述
了,与RxJava 2中的如出一辙,并且onBackpressureBuffer操作的用
法也是如此。
2.2.2 Flux的快速包装方法
下面介绍Flux的快速包装方法,主要分为支持背压的方法和不支
持背压的方法,下面分别介绍。
支持背压的方法如下。
◎ just:可以指定序列中包含的全部元素。创建的Flux源序列会
在发布元素之后自动结束。
◎ fromArray、fromIterable和fromStream:可以从一个数组、
Iterable对象或Stream对象中创建Flux对象。
◎ empty:创建一个不包含任何元素,只发布结束消息的源序
列。
◎ error(Throwable error):创建一个只包含错误消息的源序
列。
◎ never:创建一个不包含任何消息通知的序列。
◎ range(int start,int count):创建包含从start起始的
count个Integer对象的源序列。
不支持背压(想要支持背压的话,可以手动添加,调用
onBackpressureXXX方法即可)的方法如下。
◎ interval ( Duration period ) 和 interval ( Duration
delay,Duration period):创建一个包含了从0开始递增的
Long对象的源序列。其中包含的元素按照指定的间隔时间来
发布。除了间隔时间外,还可以指定起始元素发布之前的延
迟时间,指定的时间间隔和延迟时间的单位为ms。在未手动
添加背压策略的情况下,具体消费行为在有延时的情况下很
容易发生异常,其原理与RxJava 2中的一模一样。
下面看一个Demo:
另外,大家可能会对Flux.fromStream操作感兴趣,它内部主要利
用 了 stream.iterator 方 法 , 并 得 到 了 Iterator 对 象 , 实 现 方 式 与
RxJava 2 中 的
io.reactivex.internal.operators.flowable.FlowableFromIterable
基本相同。可以对比两者的源码实现,主要区别在于使用JDK提供的
Stream 时 , 需 要 在 出 现 异 常 或 结 束 操 作 的 时 候 关 闭 流 , 即 会 调 用
stream::close。
2.2.3 Reactor 3中的generate方法
图2-2是Reactor 3中generate方法的运行原理。

图2-2
generate方法通过同步(会限制对下游操作API的调度选择)和逐
一 方 式 来 产 生 Flux 元 素 序 列 。 元 素 序 列 的 产 生 是 通 过 调 用
SynchronousSink对象的next、complete和error(Throwable)方法来
完成的。逐一产生是指在具体的元素产生逻辑中,next方法最多只能
被调用一次。在有些情况下,元素序列的产生可能是有状态的,需要
用 到 某 些 状 态 对 象 。 此 时 可 以 使 用 generate 方 法 的 另 一 种 形 式 :
generate ( Callable<S> stateSupplier , BiFunction<S ,
SynchronousSink<T>,S>generator),其中的stateSupplier用于提
供初始的状态对象。在产生元素序列时,状态对象会作为generator的
第一个参数传入,可以在对应的逻辑中对该状态对象进行修改,以供
下一次产生时使用:

在上面的源码中,Reactor的generate方法和RxJava 2中的实现也
很相似,细节稍有不同,比如其实现同步的方式。下面,我将大家在
阅读源码过程中可能会产生疑惑的地方讲解一下。
首先观察以下源码:
产生订阅所执行的动作很简单,就是生产几个初始状态值,然后
调 用 消 费 者 CoreSubscriber 的 onSubscribe 方 法 , 传 入 一 个
Subscription对象,默认会调用该Subscription对象的request方法:

在这里,说明一下Operators.addCap(REQUESTED,this,n)==0
操作:
前面介绍过LambdaSubscriber,在默认的情况下,其请求数为
Long.MAX_VALUE , 也 就 是 toAdd 的 值 为 Long.MAX_VALUE 。 这 里 的
REQUESTED的初始值为0,也就是r等于0,所以会跳过第一个if语句,
接着执行addCap方法,得到的u值就是Long.MAX_VALUE。但要注意,r
依 然 等 于 0 , 因 为 基 本 类 型 参 数 属 于 值 传 递 , 所 以
updater.compareAndSet(instance,r,u)内产生的数据计算不会影
响 r 的 值 , addCap 方 法 返 回 的 就 是 0 , 这 就 是 说 ,
Operators.addCap(REQUESTED,this,n)==0这个操作为true,才会
进入下面的判断。而RxJava 2中的BackpressureHelper.add(this,
n)!=0L,其内部实现和上面所述是一致的,若请求数已经设定过,
则 r 不 等 于 0 , 此 时 BackpressureHelper.add ( this , n ) ! =0L 为
false,直接返回即可,接着Reactor会将余下的操作逻辑抽取出来,
根 据 请 求 数 是 否 为 Long.MAX_VALUE 来 选 择 执 行 fastPath 或
slowPath(n)方法:
这 里 也 是 generate 方 法 实 现 同 步 的 地 方 , 其 中 只 需 要 弄 明 白
fastPath 方 法 。 查 看 s=g.apply ( s , this ) 操 作 , 之 前 分 析 过 ,
FluxSink可以达到与RxJava 2中的FlowableEmitter相似的能力,在这
里就是SynchronousSink。那么对照RxJava 2中下发元素的动作,即在
BiFunction<S,?super Emitter<T>,S> generator的实现中只执行
一 次 Emitter 的 onNext 操 作 。 此 处 Reactor 会 执 行
GenerateSubscription # next 操 作 , 那 么 会 在 下 发 元 素 前 先 设 定
hasValue为true,然后在fastPath方法结束的时候将hasValue设定为
false。在这个过程中,假如下游有多个线程同时在执行异步接收操
作,那么只要有一个线程中的fastPath方法结束,剩下的线程在进行
if(!hasValue)判断时就会进入其执行体,从而产生异常。这也告
诉我们,此方法不支持下游subscribeOn的多线程池异步请求操作,需
要大家注意。
可以查看以下Demo,希望读者可以从这个Demo中找出上面想表达
的东西,带着问题继续读接下来的内容:
执行结果如下:
为什么没有报错?为什么没有在产生多线程的同时并发请求,而
只是相对于主线程做了异步处理?此处的“猫腻”接下来会一步步揭
晓。
为了保证此generate API的通用性,RxJava 2和Reactor都下了很
大功夫,这里也是RxJava 2中的Flowable没有涉及的地方,Reactor 3
中 的 generate 方 法 算 是 对 RxJava 2 进 行 了 一 点 优 化 。 RxJava 2 和
Reactor 3中元素生产者处理下游异步请求的代码设计原理差不多,所
以 放 在 Reactor 里 一 并 讲 解 。 在 这 里 , 两 者 都 是 通 过 一 个
QueueSubscription类型的实现类做到如下效果的:从元素生产下发到
消费的整个过程中,根据自身实际情况来协调各个操作间的同步和异
步行为。

2.3 蛇行走位的QueueSubscription
从QueueSubscription接口的注释可以知道,它主要用于对那些内
部有队列支持的subscription进行优化。
对于那些有固定数量大小的同步源序列,可以通过拉取的方式来
发射元素,在很多情况下这样避免了请求计算的开销。而那些异步源
序列,则同时扮演了queue和subscription两种角色,其中的大部分操
作都是将元素存储至一个又一个队列数组中(队列满了则再创建一个
队列插入上一个队列的队尾)。这样下发和创建数组都是需要进行计
算 的 , 比 如 前 面 介 绍 过 的
reactor.core.publisher.FluxCreate.BufferAsyncSink#next操作,
里面包含了queue.offer(t),可支持背压,然后根据元素请求数量
在一个循环体中调用queue.poll方法来完成元素的下发。
2.3.1 无界队列SpscLinkedArrayQueue
在 这 里 , 先 来 看 看 Reactor 3 中 对 RxJava 2 中
SpscLinkedArrayQueue部分的迭代。通过本节,我们可以对《Java编
程方法论:响应式RxJava与代码设计实战》“Flowable与背压”一章
的“BackpressureStrategy.BUFFER策略”一节中所述的内容有更深一
步的理解(没有看过这部分内容也不影响理解本节知识)。
首先来看看源码:
在这里,针对多线程下的安全操作,分别对索引和producerArray
进行原子化,接着设计一个无界队列,如图2-3所示。
图2-3
首先得到一个原子类型的数组,把该数组最后一个位置空出来,
用于存储下一个数组的引用。然后假设在存储一个元素A时,若发现将
该元素存储到数组中会造成数组内部空间占满,那么此时在A要存储的
位置放置一个关键字元素NEXT(一个final常量),用于标记此数组已
满,该关键字元素表示:请到本数组最后一个位置获取下一个数组的
引用和NEXT在原数组中相对位置的偏移量,并在下一个数组相同的偏
移量位置存入元素A。
在设定队列中单个数组长度的时候(假如为c),为了更好地适应
无界队列场景并确定所存储元素在数组中的位置,c的大小必须为2 。 N
因为我们要存储一个指向下一个数组的引用,同时也要存储一个标志
位NEXT,而这个NEXT又与我们所存储元素的位置相关,所以我们给设
定的数组长度加1,即c+1,并将其作为实际的数组长度,而一个数组
中实际能存储的元素数量仅为c-1,所以设定一个变量mask,通过二进
制计算来专门确定当前下发元素在数组中要存储的位置,如图2-4所示
(图中的length为我们设定的传入数组的长度)。

图2-4
在将元素存储到这个数组中的时候,如何存储NEXT标志位常量元
素呢?这时就要腾出一个位置,我们使用this.mask=c-1空出了两个位
置(对于this.producerArray数组来说,c已经代表了这个数组最后一
个 位 置 的 下 标 , 那 么 c-1 就 代 表 了 倒 数 第 2 位 的 下 标 ) 。 接 着 通 过
(index+1)&mask来确定我们所插入元素在该数组中的下一个位置的
偏移量(也可以理解为在该数组中位置的下标,此处的index是要存储
元素在整个无界队列中的位置),用来判断我们是否需要切换新的数
组。从上面的代码注释来看,假如传入的linkSize为128,那么c就是
128,数组长度就是129,而this.mask就是127,当index的值等于127
的时候,(index+1)&mask就等于0,此时指向的就是数组中下标为0
的位置。
这样,当要存储元素所在的index是一个很大的数时,就可以通过
计算得到该数在当下正在操作的数组中的偏移量(也就是数组中对应
位置的下标),接着判断其下一个位置中是否有元素(即(index+1)
&mask位置中是否有元素),这里如果执行的是添加元素操作,那么当
下一个位置中有元素时,则说明当前数组已满,这时就创建新数组b,
并将这个元素存储在新数组的偏移量大小为index&mask的位置,然后
在原数组内的这个位置存储NEXT标志位常量元素,并在原数组的最后
一个位置存储新数组的引用,将这个新数组b赋值给
this.producerArray 。 因 为 第 一 个 原 数 组 的 引 用 已 存 储 于
this.consumerArray(可查看上面定义的构造器),所以不用担心其
会丢失。
N
之所以要求必须使用2 的长度,是因为只有这样,在长度减1之后
才能得到一个以0开头后面全都是1的二进制数,在使用任意合法范围
内的正整数与之进行&运算时,所得结果都会在0至2 -1范围内。 N
在这里,有读者可能会产生疑问,如果有一个b数组,可参考图2-
4中下面的数组,NEXT标志位常量元素就不能存储于倒数第2位了(因
为倒数第2位已经被触发创建b数组的那个元素所占据),所以这个标
志位常量元素索性就存储于b数组的倒数第3位,然后在下一个新数组c
的倒数第3位存储新元素,当又有一个新元素要往数组c中存储的时
候,其会存储在数组c中的倒数第2位,再来一个新元素就存储于c的正
数第1位(也就是c数组中下标为0的位置),依此类推。最后那个位置
总是固定空出,留给下一个数组的引用,而NEXT标志位常量元素的意
义是,你应该去下一个数组的这个偏移量位置找元素。
注意:请区分总index长度和相对每个数组的index长度(这个相
对index长度就是具体每个数组的偏移量offset),这对理解通过
SpscLinkedArrayQueue中的poll方法来拉取元素的过程很有帮助。
接下来看看源码实现:
在这里,利用原子类控制来实现了多线程操作下的安全保障,利
用&操作来达到了类似于环形队列般的位置锁定(类似于%取余操
作),也提高了底层代码的性能,毕竟数组的创建和存储,以及获取
元素的操作,都需要很大的计算量。最后,利用NEXT常量元素来做标
志位,标记下一个元素位置。这样可以穿针引线,达到了蛇形走位的
目的。
2.3.2 QueueSubscription.requestFusion的催化效应
有些时候,队列只是一个形式,其并不会真正地产生、存储或获
取元素,更多的是为了匹配大的架构代码规则。可以思考一下,当基
于对状态值感应的情况下发元素时,真的不需要存储元素,从主观的
角度来说,这里无法接受多个线程同时请求下发元素(虽然在RxJava
2中是可以做到的),也就是说Reactor 3和RxJava 2不一样,Reactor
3中的实现过程和RxJava 2相比,加了一个布尔型变量的判断控制。那
么该如何将消费控制在一个单线程中呢?即便加入调度操作也不好
使,在不改变RxJava 2源码大的格局形式的情况下,该如何对Reactor
3进行创新(应该说在源头进行Bug的修复,毕竟多线程请求很容易发
生状态值异常,这点有多线程编程基础的读者都应该很清楚)呢?下
面就来看看QueueSubscription.requestFusion带给我们的催化效应
吧。
先来看一段源码:
在这里,可以看到peek、add、offer这3个方法都是默认方法,而
且这3个方法的默认实现都是抛出异常,它们就是用来给
QueueSubscription接口定基调的东西,告诉我们这个接口并不支持我
们所熟悉的背压。结合上下文可知,它需要一个用来判断是同步还是
异步请求拉取策略的方法,这就是requestFusion。
从int requestFusion(int requestedMode)方法的注释可以知
道,作为一个订阅者(即消费者),下游操作会调用上一个操作实现
的 requestFusion 方 法 , 你 传 入 的 参 数 所 代 表 的 模 式 可 以 从 SYNC 、
ASYNC或者ANY等任选其一(但绝不能是NONE)。而上游的操作对于
requestFusion方法的具体实现,所返回的模式结果应该是NONE、SYNC
或ASYNC三者之一(但绝不能是ANY)。
到此可以知道,requestFusion方法就是用来做策略判断的,根据
订阅者(也就是中间操作)所传入的支持的模式,源做出相应的反
馈,比如支持异步request请求的话就回馈一个ASYNC,也就是说,这
个模式归根结底还是根据源所支持的类型来进行选择的。
下面来看看FluxGenerate.requestFusion的实现:
可以看到,其策略是返回一个Fuseable.NONE,因为RxJava 2的
FlowableGenerate.GeneratorSubscription 并 没 有 实 现
BasicIntQueueSubscription接口(我们可以理解为RxJava在此不做请
求 模 式 设 定 ) , 所 以 Reactor 3 中 GeneratorSubscription 实 现 的
QueueSubscription接口表示在非特殊情况下不对模式做任何设定,在
requestFusion方法实现中,if条件语句不成立的情况下,默认返回一
个NONE策略标志。
至此,对QueueSubscription的讲解暂时告一段落,更多关于其在
多线程下的实际应用,将放在讲解调度器的时候具体讲解。

2.4 Mono的二三事
一个Flux代表一连串指定类型的事件,而且Flux中有很多方便的
静态方法,可以用于创建各种我们需要的源的实例。通常,你会碰到
只有0个或1个元素的源序列。举个例子,当根据用户的id查找用户的
时候,其返回结果要么为空,要么是一位用户。在Reactor中,可以使
用Mono来处理这一情况。Mono对源进行创建的方式与之前介绍的Flux
比较相似,可以类比RxJava中的Flowable和Single,Single用于处理
单值源序列,与Mono很相似。在RxJava中,可以使用Completable来专
门应对空序列或者只关注源下发结束的状况。假如这都觉得麻烦的
话,那么直接选用Maybe吧,其属于Single和Completable两者的结合
体 , 可 以 发 射 0 个 、 1 个 或 错 误 的 事 件 。 Mono 类 中 也 包 含 了 just 、
empty、error和never等一些与Flux类中相同的静态方法。除此之外,
Mono中还有一些独有的针对自己特性的静态方法,分别介绍如下。
◎ fromCallable 、 fromCompletionStage 、 fromFuture 、
fromRunnable 和 fromSupplier 方 法 : 分 别 从 Callable 、
CompletionStage 、 CompletableFuture 、 Runnable 和
Supplier中创建Mono对象。
◎ delay(Duration duration)方法:创建一个Mono源序列,在
指定的延迟时间之后,产生数字0作为唯一元素下发,然后调
用actual.onComplete。
◎ ignoreElements ( Publisher<T> source ) 方 法 : 创 建 一 个
Mono源序列,忽略作为源的Publisher中的所有元素,只产生
结束消息(其内部onNext方法的下发操作为空实现,代表忽
略)。
◎ justOrEmpty ( Optional< ? extends T> data ) 和
justOrEmpty(T data)方法:从一个Optional对象或可能为
null的对象中创建Mono。只有Optional对象中包含的值或对
象不为null时,Mono源序列才产生对应的元素。
同样,可以通过create方法来使用MonoSink创建Mono。
在这里,就不对其源码进行深入分析了,感兴趣的读者可以自行
查阅源码,下面只给大家准备一个Demo展示一下:

接下来,会介绍一些比较实用的内容,可方便读者在项目中根据
具体情况使用。

2.5 通过BaseSubscriber自定义订阅者
在前面的例子中,一直都通过在subscribe方法中添加参数来定义
Subscriber,那么在这里会通过继承BaseSubscriber来实现一个自定
义的Subscriber,请观察下面的简单实现:

为什么要重写这两个方法呢?下面先试着观察一下
BaseSubscriber中我们经常会关心的几个点:
可以知道,在参与订阅的时候,会先调用onSubscribe方法,通过
这个回调方法就可以很容易地定义是使用推还是拉的方式。如果使用
的是拉的方式,那么就在hookOnSubscribe回调方法内进行request方
法调用。然后,执行重要的方法onNext,其中包含最重要的消费逻
辑,所以必须重写hookOnNext回调方法。其实hookOnXXX方法都是空实
现,需要根据自己的实际情况加以重写。下面展示一个很简单的实
现,就不多做解释了。
于是,可以进行如下操作:

输出如下:
同 样 , 也 可 以 在 源 的 subscribe 方 法 中 使 用 匿 名 类 实 现
BaseSubscriber:

代码执行完毕,会得到与上面一样的结果,这里在
hookOnSubscribe 中 使 用 了 requestUnbounded 方 法 , 这 也 是
BaseSubscriber提供的直接以Long类型的最大值进行请求的方式,其
实际上还是调用了request(Long.MAX_VALUE))。这样,无形中就又
回到了主动推的PUSH模式了。

2.6 将常见的监听器改造成响应式结构
很多读者对监听器一直都比较抗拒,其实完全没必要,你只需将
它看作容器里的一个bean即可。其注册过程,其实就是将这个bean添
加进一个容器的过程,这个容器可以是一个List对象,也可以是一个
Set对象,还可以是一个Map对象,根据不同的使用场景而变化。
当产生事件时,根据事件类型,获取相应的支持处理该类型的监
听器,得到监听器后进行遍历处理即可。此处不打算编写太复杂的
Demo,监听器的注册过程只是一个简单的赋值操作,下面先定义接
口:

然后,对MyEventProcessor接口进行实现:
在这里,通过register(MyEventListener eventListener)方法
定 义 了 注 册 过 程 , 通 过 dataChunk ( String...values ) 方 法 下 发 事
件。我们把这个下发事件的过程放在一个全新的线程中执行,这样可
以达到异步效果,同样processComplete也是由所定义的executor执行
的。
最后,对监听器MyEventListener进行响应式的异步实现:
我们创建了一个源,自定义源的核心逻辑在于next和complete操
作,其下发类型由next方法所传递的参数类型决定。确定了这些后,
下面需要确定业务逻辑,主要是注册操作。然后思考,平时监听器需
要一个相应的Handler来处理事件,那么此处为了简化逻辑,将本应由
Handler处理的直接由监听器对应的方法实现,主要是处理、消费所下
发的事件。
假如是在Spring中操作,可以理解为给此监听器内的相应Handler
进 行 赋 值 。 假 如 这 个 监 听 器 对 应 了 一 个 List 集 合 的 事 件 处 理
Handlers,那么就可以在自定义的Subscriber的onNext方法内根据特
定匹配条件来选择最合适的Handler。
我们之前介绍过FluxCreate中的源码,在这里,回顾一下与此处
内容相关的部分源码:

可以看到,只有在产生订阅的时候才会执行Consumer<?super
FluxSink>source 所 代 表 的 逻 辑 。 对 于 监 听 器 Demo 来 说 , 通 过
Flux.create得到的源会在产生订阅后引发执行source.accept,这时
相应Handler的定义才会正式注册,在这里不会为新建的监听器对象分
配内存,以此做到按需分配内存,提高程序性能。理解这个逻辑之
后,可以知道事件分发也只有在产生订阅之后才可以进行,即调用
myEventProcessor.dataChunk("foo","bar","baz");同样,若想
解 除 监 听 , 调 用 Flux.create 所 传 参 数 中 定 义 的
myEventProcessor.processComplete即可解除订阅关系。

2.7 Flux.push的特殊使用场景及细节探索
专门讲解Flux.push方法,是为了解决大家可能会产生的疑惑。下
面先来看一下方法定义:

可以说,Flux.push是Flux.create的一个变种,其只适用于处理
单个线程下生产者产生的事件。回顾一下前面subscribe方法的源码可
以 知 道 , 其 中 创 建 了 一 个 BufferAsyncSink 对 象 , 通 过
CreateMode.PUSH_ONLY 确 定 了 createMode :
createMode==CreateMode.PUSH_PULL ? new
SerializedSink<>(sink):sink,也就是直接使用的是原汁原味的
BufferAsyncSink对象。这也就是说,其依然是支持背压的,并非遵照
字面意思Push而只支持推送,而且因为其只适用于单个线程下的生产
者,所以一次只能由一个生产线程调用next、complete或error方法,
这样也就不必为元素异步下发而担忧。大家可能会产生疑问,
SerializedSink做了什么呢?下面来看看关键的源码部分:
可以看到,这里定义了一个volatile变量wip,其使用原子类的方
式来进行操作,在next方法中通过WIP的CAS操作来保证一次只能有一
个元素下发,元素下发后将WIP再减1,这样其就会被重置为0。假如同
时有多个源在不同的线程中并发产生元素,那么发生冲突后,CAS操作
失败的那个源会直接将元素加入备用队列中,以此来达到对多个源产
生的元素执行序列化下发操作的目的。
假如很确定生产端只有一个线程在生产元素,那么完全可以抛弃
上述包装,直接使用Flux.push,这样既可以达到我们的目的,还能减
少一层包装并提高性能。

2.8 对Flux.handle的解读
Flux.handle方法确实有点特别,它在Mono和Flux中都存在,下面
先来看看其源码:
从方法参数、实现及注释,可以很清楚地知道,Flux.handle只是
一个中间操作而已,但为什么要将其单独拿出来讲解呢?因为其不是
源产生者但似源产生者。由方法中的注释可知,在上游源下发元素并
调用FluxHandle中包装的订阅者HandleSubscriber的onNext方法时,
才会调用biconsumer,而这个biconsumer的内部最多只能调用一次
SynchronousSink # next ( Object ) , 其 中 会 调 用 0 个 或 者 1 个
SynchronousSink#error(Throwable)(在产生异常的情况下)或
SynchronousSink#complete(可定义在某些情况下直接下发结束)操
作。
接下来,深入探索其中的源码设计:
此 处 的 代 码 逻 辑 比 较 容 易 , 实 例 化 FluxHandle 时 , 只 对 源 和
handler进行了赋值。在产生订阅时,才会触发具体的操作逻辑。我们
直 接 关 注 HandleSubscriber 即 可 , 因 为 HandleSubscriber 也 是
ConditionalSubscriber的一种实现,只不过它是根据订阅者的类型需
求差异而进行的二次包装。
下面来看看相关源码:
再 次 回 顾 之 前 的 知 识 , InnerOperator 接 口 集
CoreSubscriber<I>、Scannable、Subscription三者的功能于一体。
从 reactor.core.publisher.FluxCreate.BaseSink 可 以 知 道 ,
SynchronousSink<R> 在 实 现 了 FluxSink 、 Subscription ( 通 过
InnerProducer接口可知)接口后才可以下发元素。那么,这里同样需
要有一个SynchronousSink实现,这样在调用BiConsumer<?super T,
SynchronousSink<R>> 函 数 式 动 作 的 时 候 , 我 们 在
FluxHandle.subscribe中设定的HandleSubscriber对象就可以作为第
二个参数传入BiConsumer了。对于使用者来说,只需要专注于自己具
体想要实现的业务过程即可,而无须操心SynchronousSink的实现,这
也符合开放-封闭原则。
下面再来讲解一下这里在设计订阅者时的思路:根据订阅需求,
需要实现一个订阅者接口CoreSubscriber,根据拉取策略,需要实现
一个Subscription,所以我们定义的订阅者直接实现InnerOperator接
口即可,然后根据具体的操作场景,这里主要针对的是所传入的
BiConsumer中的SynchronousSink,假如让使用者自行实现,无疑加大
了难度和风险,很多开发者很难处理好其中的细节,所以需要按照开
放-封闭原则进行处理。这里就是在设计订阅者的同时实现了
SynchronousSink接口。
于是,在自定义类似的源或操作的时候,请按照下面给出的代码
思路进行设计实现:
在 这 里 , 通 过 HandleSubscriber 可 以 看 到 HandleSubscriber #
next等对SynchronousSink接口的实现,同时HandleSubscriber作为
CoreSubscriber,又可以看到其中的HandleSubscriber#onNext的实
现。这里只展示了这两个逻辑,其他逻辑大家可以自行深入了解。
可以看到,在HandleSubscriber#onNext实现里,BiConsumer<?
super T,SynchronousSink<R>>handler接收了两个参数,其中一个是
上 游 源 正 常 下 发 的 元 素 , 而 HandleSubscriber 类 实 例 以
SynchronousSink的身份作为另一个参数传入handler中。这样,在执
行 handler 的 时 候 , 其 中 的 sink.next 就 表 示 调 用 的 是
HandleSubscriber 的 next 实 现 ( 如 上 面 的 源 码 所 示 ) , 它 会 将
BiConsumer<?super T,SynchronousSink<R>>实现逻辑中的处理结果
赋值给HandleSubscriber中定义的data变量,然后衔接中间操作所传
入的真正的订阅者,调用其onNext方法actual.onNext(v),将data
变量的值下发下去。
下面通过一个Demo来看一下具体的应用:
执行结果如下:

2.9 小结
至此,对Reactor中源的订阅和创建的基本细节已经描述完毕,我
们谈及了很多源码细节,介绍了很多实用的代码使用技巧,这些内容
都需要读者反复“消化”并应用到自己的项目实战中。接下来,会介
绍之前在generate相关章节最后的Demo中涉及的关于调度的内容,在
《Java编程方法论:响应式RxJava与代码设计实战》一书中并未对调
度器进行源码级的实现,那么在本书第3章中,将带着大家深入调度器
的各个细节,并解决其使用过程中可能存在的问题和解答读者的疑
惑。
第3章 调度器
《Java编程方法论:响应式RxJava与代码设计实战》一书中介绍
了调度器的概念,Reactor 3和RxJava 2中的调度器实现是很相似的,
不过Reactor 3又对其进行了不少重构,更加匹配全新的JDK版本。通
过调度器(Scheduler),可以指定操作执行的方式和所在的线程。下
面介绍几种不同的调度器实现。
◎ 在当前线程中调度任务的调度器,通过Schedulers.immediate
方法来创建。
◎ 通过单一的可复用的线程调度任务的调度器,通过
Schedulers.single方法来创建。需要注意的是,当各个调用
者调用Schedulers.single方法后,都会重用同一个线程,直
到这个Scheduler的状态被设定为disposed。如果你希望为每
次 调 用 都 指 定 一 个 全 新 的 线 程 , 可 以 使 用
Schedulers.newSingle方法。
◎ 使用弹性线程池调度任务的调度器,通过Schedulers.elastic
方法来创建。该调度器角色与RxJava 2中的Schedulers.IO发
挥 的 作 用 一 样 。 当 首 次 需 要 调 用 Schedulers.elastic 方 法
时,会创建一个新的线程池,而且这个线程池中闲置的线程
可以被重用。如果一个线程的闲置时间太长(默认时间是
60s),则会被销毁。该调度器适用于处理与I/O操作相关的
操作,Schedulers.elastic方法确实是一种将阻塞处理放在
一个单独的线程中执行的很好的方式。
◎ 一个为需要执行并行任务而设计的,拥有固定线程数量的线程
池调度器,通过Schedulers.parallel方法来创建。其中的线
程数量取决于CPU的核数。该调度器适用于处理计算密集型任
务。
◎ 从 已 有 的 ExecutorService 对 象 中 创 建 调 度 器 , 通 过
Schedulers.fromExecutorService(ExecutorService)方法
来创建。
◎ 同样,也可以使用newXXX来创建各种类型的全新的scheduler
实 例 。 例 如 , 通 过
Schedulers.newElastic(yourScheduleName)方法,可以创
建一个全新的名字为yourScheduleName的elastic scheduler
实例。
某些操作默认已经使用了特定类型的调度器。比如,使用
Flux.interval(Duration.ofMillis(300))方法创建的源,就使用
了由Schedulers.parallel方法创建的调度器,可以通过操作来人为地
改 变 调 度 器 实 例 : Flux.interval ( Duration.ofMillis ( 300 ) ,
Schedulers.newSingle("test"))。同时,也可以通过publishOn和
subscribeOn方法切换执行操作的调度器。其中,publishOn方法切换
的是元素消费操作执行时所在的线程(与RxJava 2中的Flowable#
observeOn发挥的作用相似),而subscribeOn方法切换的是源中元素
生产逻辑执行时所在的线程(其同样也与RxJava 2中的Flowable#
subscribeOn发挥的作用相似)。
接下来,将对Reactor 3改动RxJava 2中的设计较多的部分,以及
《 Java 编 程 方 法 论 : 响 应 式 RxJava 与 代 码 设 计 实 战 》 一 书 中 的
Flowable部分并未涉及的一些点,来进行深入讲解。

3.1 深入理解Schedulers.elastic
关于调度器的策略有很多,下面只对Schedulers.elastic进行深
入解读,其他策略都大同小异,只是在实现细节上会有一些区别,限
于篇幅不再赘述。
下面看看相关源码:
可以看到,elastic方法调用了cache方法,然后传入了相应的策
略 。 关 于 这 一 点 , 也 可 以 关 注 一 下
reactor.core.scheduler.Schedulers # single ( ) :
cache ( CACHED_SINGLE , SINGLE , SINGLE_SUPPLIER ) 和
reactor.core.scheduler.Schedulers # parallel ( ) :
cache(CACHED_PARALLEL,PARALLEL,PARALLEL_SUPPLIER),它们其
实是一类方法,只是采用的策略不一样。
接 着 , 从 cache 方 法 可 以 看 到 , 其 通 过 接 收 一 个 引 用 原 子 类
AtomicReference reference 获 取 了 缓 存 的 CachedScheduler ( 没 有
CachedScheduler的话,就临时创建一个新的,这也符合前面介绍的首
次调用时创建而后续重用的原则)。当不需要创建CachedScheduler的
时候,就接收一个supplier.get,其Supplier<Scheduler>supplier是
一个传入动作,按需加载,俗称懒加载,这样可以避免浪费计算资源
(其若作为函数参数,则无须直接计算结果,否则就要先计算出值,
如将a+b作为参数传递)和减少JVM占用空间,毕竟ELASTIC_SUPPLIER
是Schedulers.java的静态final字段。
3.1.1 CachedScheduler的启示
在 这 里 , CachedScheduler 会 作 为 适 配 模 式 对 Scheduler 进 行 适
配。从CachedScheduler的角度来看,就是适配Scheduler的多样性,
其内部实现很简单,所有代码逻辑直接由具体类型的Scheduler来实
现。
只需查看具体的Scheduler实现就好,结合ElasticScheduler,即
关注ELASTIC_SUPPLIER的实现:
在这里,我将涉及的相关点放在一起展示给大家,可以看到上面
整个过程只是为了得到一个ThreadFactory实例,然后加入一个默认闲
置 时 间 DEFAULT_TTL_SECONDS , 两 者 一 起 可 以 构 造 出 一 个
ElasticScheduler实例。从中可以学到,假如想要自定义所创建线程
的一些信息(比如自定义线程名称、是否是非阻塞线程、是否为守护
进程,以及自定义线程的异常处理方式),就可以通过实现
ThreadFactory接口来完成。
而reactor.core.scheduler.Schedulers.Factory接口里都是一些
default默认实现方法,这也是为了API设计的美观和统一。比如,对
于 上 面 源 码 中 newElastic ( int ttlSeconds , ThreadFactory
threadFactory)方法中的实现,就无须专门去API中找那些具体实
现,而是通过这个接口使开发者可以很方便地获取到这些实现,这也
方便后续迭代,比如废弃和新增全新的实现。
3.1.2 ElasticScheduler的类定义思路
接下来,将深入了解ElasticScheduler的设计和实现。首先,思
考一个场景,我手里有很多任务,在一个房间里有一群小伙伴可以做
这些任务,那我可以将任务交给其中一个小伙伴来做。这个场景涉及
几个元素:任务、任务执行者(即每一个小伙伴)、承载任务执行者
的容器(房间)。最后就将任务提交给容器里的任务执行者来执行。
我们接下来可以设计一个调度器。调度器可以提交任务,它应该
有一个管理任务执行者的容器,每一个任务执行者应该包裹一个线
程。对于人来说,一个人可以手里做着一个任务,同时提前接收自己
可以承受的其他任务,为了尽量贴合现实,此处的任务执行者也具备
这个属性,所以这个任务执行者应该是一个有且只有一个线程的单线
程线程池。这样,调度器可以在容器中选择一个任务执行者,并将任
务提交给它来处理。
结合上面的思路,在设计接口的时候,这个调度器接口应该具有
一个调度任务的方法(其实也就是将任务提交给某个任务执行者),
也应该有获取一个任务执行者的方法(当然,任务执行者也需要配合
这个接口一起定义,任务执行者应该具备的核心方法也需要定义),
还应该有在需要放弃这个调度器时结束并释放相关资源的方法。
那么就来看看Scheduler的接口定义:
接着看看官方对此接口定义的注释:

从注释可以知道,这个接口用于使操作异步化,可以通过
Schedulers#decorateExecutorService(String,Supplier)提供的
Supplier 参 数 来 获 取 ExecutorService 接 口 或
ScheduledExecutorService实现类的实例。
所以ElasticScheduler类就需要实现Scheduler和Supplier接口,
后 者 是 为 匹 配 Schedulers # decorateExecutorService ( String ,
Supplier)的第2个参数而设定的。同时,为了获取操作过程中的一些
信息,这里会实现Scannable接口。关于此接口,不会展开介绍,对比
各个操作中的实现,就会明了其中的原理,故不再赘述。
于是,查看下面类的定义:

3.1.3 对Schedulers.decorateExecutorService的解读
在 使 用 JDK 提 供 的 线 程 池 提 交 任 务 的 时 候 , 我 们 往 往 会 用
ScheduledExecutorService的schedule或submit方法来操作。这里也
不例外,再结合Scheduler接口的定义分析可知,最后应该会使用通过
Schedulers#decorateExecutorService(String,Supplier)得到的
ScheduledExecutorService exec 来 提 交 任 务 :
exec.submit ( ( Callable< ? > ) sr ) 。 由 于
ScheduledExecutorService属于大对象,因此创建起来很麻烦,我们
希望可以重复利用它,其可以对任务执行器进行缓存或超时释放管
理。为了适配这个场景,设计了CachedService静态内部类:
到这里,其实就很简单了,判断所传入的ElasticScheduler实例
参数是否为null,不是的话,直接将其拿来复用即可。下面可以看看
decorateExecutorService的具体实现:

可以看到,上面直接调用了Supplier的get方法,而且我们已经为
ElasticScheduler实现了Supplier接口,下面看看其具体实现:

可以看到,每调用一次decorateExecutorService方法,都会返回
一 个 新 的 poolExecutor , 而 且 这 个 线 程 池 里 只 有 一 个 线 程 。
CachedService将对任务执行器进行的缓存或超时释放管理放在了它的
dispose方法中,后面的内容中会涉及该方法。
3.1.4 对ElasticScheduler.schedule的解读
接着,我们来查看ElasticScheduler作为Scheduler角色时,最重
要的schedule方法的实现:

首先会看到一个关键的抽取方法pick,具体源码如下:
在这里,就可以将之前提及的独立的点给联系起来了。cache会在
ElasticScheduler类初始化的时候被赋值为null,所以在第一次调用
pick的时候e绝对为null,于是会创建CachedService实例,代码中的
all是用来方便做移除工作的。而cache.offer方法会在后面进行调
用,而不是在这里,所以后面再进行具体的讲解。
接着回到schedule方法中,可以发现Schedulers.directSchedule
中传入了cached.exec,这个静态内部类中的字段没有加修饰符,即这
个字段拥有包访问权限。这点需要注意,这也是静态内部类常用的方
法之一,经常会用来提高包的内聚性和增强对包外的封闭操作。
3.1.5 对ElasticScheduler.DirectScheduleTask的解读
接 着 , Schedulers.directSchedule 中 还 传 入 了 new
DirectScheduleTask(task,cached)实例,这也是核心点,毕竟这
里是对自定义任务进行包装的地方:
可以看到,上述源码对我们传入的Runnable对象执行了增强操
作,而我们关注的是其run方法内的finally操作,这个操作很重要,
即执行cached.dispose方法。下面来看看这个dispose方法为什么重
要:
也就是说,在任务执行完毕后,要进行最终的判断,假如
cached.exec 不 为 null , 而 且 并 没 有 SHUTDOWN , 那 么 就 要 对
ExecutorService 设 定 闲 置 过 期 的 效 果 。 此 处 通 过
ScheduledExecutorServiceExpiry包装类进行实现,以ms为单位。其
目的就是判断一个过期时间,所以只需简单包装一下,然后将之添加
到ElasticScheduler管理的任务执行器缓存队列中,以待重用:
另 外 , 在 最 新 的 Spring Reactor 中 ,
ElasticScheduler.DirectScheduleTask 已 被 优 化 , 但 本 节 所 述 的
CachedService#dispose代码逻辑依然有效,大家可对比新版本中的
优化代码,也可结合我分享的相关视频内容加深对相关迭代代码逻辑
的理解,毕竟在DirectScheduleTask的run方法中cached.dispose方法
使调度清理工作与业务的任务间产生了耦合。
3.1.6 对Schedulers.directSchedule的解读
最后来看看scheduler.Schedulers#directSchedule的实现:

关于SchedulerTask,比较简单,就是针对相关执行任务所涉及
Future 的 管 理 , 这 里 要 注 意 的 是 , 在 SchedulerTask sr=new
SchedulerTask(task);中,SchedulerTask内的future字段会通过
sr.setFuture(f)进行设定,大家在读SchedulerTask源码的时候务
必注意这一点。
3.1.7 对ElasticScheduler.ElasticWorker的解读
由 前 面 的 内 容 可 以 知 道 ,
reactor.core.scheduler.Scheduler.Worker 也 是 Scheduler 接 口 内 的
核心实现之一。前面主要介绍任务是怎么一步步执行的,但是却没有
考虑如果同时有很多任务的话该怎么做。可能有读者会说,线程池中
不是有任务队列来处理这种情况吗?但大家可能忘了一点,那就是线
程池都是公用的,不一定只有一个发布-订阅关系在JVM里运行。所以
这里要强调一下,如果我们面临的是计算型任务,那么,就会选择使
用 ParallelScheduler , 它 在 一 开 始 就 创 建 了 一 个 数 量 为 的 n
ScheduledExecutorService 池 ( 每 一 个 ScheduledExecutorService 为
单线程线程池)。在创建ParallelScheduler下的Worker对象时,会从
n
这个数量为 的单线程线程池中选择一个,作为Worker自己的任务执行
器。不同的发布-订阅关系可能选择同一个ScheduledExecutorService
执行器,此时由于这个ScheduledExecutorService为单线程线程池,
多个发布-订阅关系会共用同一个ThreadLocal,如果每个发布-订阅关
系都将一些中间处理数据放在ThreadLocal中,就会发生内存泄漏,因
此Reactor专门针对发布-订阅设计了基于订阅者的Context。关于这
点,我们会在第8章中进行具体的讲解。
这 里 还 有 一 个 问 题 , 如 果 我 们 想 要 查 看 这 个
ScheduledExecutorService单线程线程池中的某个发布-订阅关系下的
任务进度,该怎么办?这时我们就要考虑为Worker设计一个专门的队
列来管理自己旗下的任务,我们可以在Worker的实现类中设定一个
Composite字段,然后通过实现Scannable接口提供的scanUnsafe方法
来获取该订阅关系在Worker中属于它自己的剩余任务数量。这也是
reactor.core.scheduler.Scheduler.Worker 设 计 的 意 义 所 在 。 这 时
Worker的具体实现也就呼之欲出了,结合当前所述思路来看看下面的
ExecutorServiceWorker源码:
在ElasticScheduler中,由于每个订阅关系在调用createWorker
方法时,都会执行pick操作,因此本质上若从ElasticScheduler的
cache中获取不到ExecutorService对象,就创建一个新的,这样也就
不会发生同一时间两个订阅关系使用的是同一个ExecutorService对象
的 情 况 了 ( ElasticScheduler 管 理 的 cache 类 型 为
ConcurrentLinkedDeque,这很好地应对了多线程下并发访问cache的
情况)。所以对于Worker的需求不是很强,但为了符合Schedules API
的 标 准 , 即 workerSchedule ( ScheduledExecutorService exec ,
Disposable.Composite tasks , Runnable task , long delay ,
TimeUnit unit),也做了相应的实现。
1.对Disposables.composite的解读
在这里,看看ElasticWorker对Worker的实现:
当一个任务结束的时候,通过继承AtomicBoolean来进行简单的控
制。在上面的ElasticWorker#dispose中,可以很明确地看到,这个
方 法 调 用 了 ElasticWorker 作 为 AtomicBoolean 原 子 类 角 色 的
compareAndSet(false,true)方法,用来进行状态控制。
在这里,可以看到任务tasks来自Disposables.composite。有一
个细节需要大家回想一下,之前有说到SchedulerTask,在Schedulers
#directSchedule方法中,其对所传入的Runnable类型的任务进行了
二 次 包 装 , 然 后 将 其 传 入 ExecutorService # submit 中 , 通 过 查 看
final class SchedulerTask implements Runnable , Disposable ,
Callable<Void>可知,此任务包装实现了Disposable接口,那么接下
来的事情就容易了。
下面看看Reactor 3对Disposable接口的定义:

与io.reactivex.disposables.Disposable相比,这里多了一个针
对task的应用描述。
而且Reactor 3对Disposable的定义也相对复杂一些,其内部根据
不同场景衍生了两个功能接口:Swap和Composite。其中Swap是一个
Disposable类型的容器,可以用来更新和替换容器内的Disposable对
象,但它不是我们关注的重点。下面来看看Composite接口的定义:
可以看到,Composite是一个Disposable容器,其同样也是一个
Disposable对象。在这里,通过调用它的dispose方法就可以将这个容
器 里 所 有 的 Disposable 对 象 的 状 态 设 定 为 Disposed 。 通 过
add(Disposable)方法将一个Disposable对象交给Composite对象进
行全权管理。若想将某个Disposable对象从这个容器中移除,就调用
其remove(Disposable d)方法,这样就可以达到自治的效果。需要
注意的是,一旦调用了Disposable.Composite#dispose方法,那么这
个容器就不能再重用了。若还想使用这个容器,那么需要重新创建一
个Composite。
对 于 Disposable 对 象 的 实 现 代 码 , 可 以 从
reactor.core.Disposables类中查找,而其中的方法名称也很直接,
想 获 取 上 面 所 说 的 Swap 和 Composite 实 现 , 可 以 直 接 调 用 swap 和
composite方法。下面查看一下composite方法的实现:
大家可以自行查看CompositeDisposable的源码实现。在默认的情
况下,只会创建一个默认的CompositeDisposable对象,其内部是用一
个数组来存储这些Disposable对象的。
2.对ElasticWorker.schedule的解读
从 前 面 讲 解 的 ElasticScheduler #
schedule(java.lang.Runnable)可知,其内需要从CachedService获
取ScheduledExecutorService对象来执行所包装的Runnable任务。
同样,ElasticWorker.schedule也逃不过“终极套路”,需要在
ElasticWorker的构造器中传入一个CachedService类型的对象,然后
在构造器中初始化任务。通过ElasticWorker.schedule的源码实现,
关注Schedulers.workerSchedule的实现,其中对任务和任务集进行了
包装、建立起联系,这样方便WorkerTask中提交的任务结束后,解除
这个任务与任务集的关联,也就是调用remove方法。相关源码如下:
上面的源码在包装WorkerTask之后,就将任务添加进任务集中
了,接下来提交、执行任务,相关流程与之前提交单个任务的流程是
一样的。
最后,看看任务和任务集之间的内部联系:
希 望 大 家 可 以 通 过 上 述 源 码 回 顾 一 下
AtomicReferenceFieldUpdater的用法,任务和任务集之间的内部联系
就体现在call方法的finally语句块中,而执行线程执行器的时候会调
用其作为Callable角色的call方法。
3.1.8 ElasticScheduler小结
至此,我们对ElasticScheduler的方方面面都进行了讲解,其他
调度器从复杂度上说都比这个调度器要低,所以不再赘述,读者感兴
趣的话可以自行探索,也可以观看本书配套视频的相应内容,其中对
此处的源码设计进行了完全解读。接下来,将接触到与调度器非常相
关的两个常用操作:publishOn和subscribeOn。

3.2 深入解读publishOn
本 节 会 再 次 讲 解 与 QueueSubscription.requestFusion 相 关 的 内
容,开始阅读以下内容之前可以回顾一下前面的相关内容。同时,本
节会展示关键的源码,其与RxJava 2中的Flowable#observeOn实现很
相似。另外,在《Java编程方法论:响应式RxJava与代码设计实战》
一 书 中 , 只 对
io.reactivex.internal.operators.observable.ObservableObserveO
n进行了简单讲解,并不够深入,在Flowable相关章节中也未涉及这部
分内容,因此本节会结合之前的内容进行更深入的解读,并告诉读者
在未来开发中可能会遇到的各种不可思议的状况。
3.2.1 publishOn流程概述
为了方便理解,首先通过图3-1来展示publishOn操作的行为细
节。
图3-1
publishOn与其他操作一样,处于整个订阅操作链中。其从上游源
获 取 元 素 , 然 后 从 所 关 联 的 Scheduler 中 获 取 一 个 worker , 调 用
worker.schedule并向下游下发元素。在RxJava 2中,定义的顺序是从
前往后,而执行的顺序则是从后往前,先对订阅者进行包装,也就是
说,在publishOn内,会将下发元素的消费逻辑交给worker.schedule
执行,即元素的消费逻辑都会在这个worker所携带的线程中执行(假
如后续没有再次调用publishOn来执行线程调度操作的话)。图3-1中
有串起元素的上下两个箭头,分别代表两个不同的线程,在从第一个
圆圈开始的元素进入publishOn后,这些元素就会在下面这个箭头代表
的线程中进行消费(注意:下面第一个圆圈左侧的线和上面箭头所在
的线属于同一个线程)。
下面来看看publishOn的源码:
在平时的开发工作中,只需要传入一个Scheduler,无须指定队列
大小,保持默认设置即可。在上面源码的最后,可以看到
Queues.get(prefetch),其返回了一个Supplier<Queue<T>>类型的
函数表达式:
从 这 里 可 以 看 出 , 默 认 获 取 的 是 SMALL_SUPPLIER , 而
SpscArrayQueue是一个有界队列。之前对SpscLinkedArrayQueue无界
队列有过详细的分析,而有界队列的实现更简单,这里就不再深入讲
解了。此处想要表达的是,在已经使用了Reactor 3的情况下,完全可
以使用其提供的现成的队列工具类,包括之前接触过的调度器的工具
类实现,这些对我们未来开发自己的基础代码来说也是很有借鉴意义
的。
上面的源码中之所以传入了一个Supplier<Queue<T>>类型的函数
表达式,是因为其内部要实现一个大量占据内存的队列,所以我们按
需加载,只有在产生订阅的时候,才真的进行内存分配,提高程序性
能。注意,我们在自己的代码中遇到类似情况时也可以采用这种方
法。
3.2.2 对FluxPublishOn的解读
接下来,对FluxPublishOn的源码进行解读:
从上面的源码可以知道,其实现subscribe的逻辑和RxJava 2中的
flowable.FlowableObserveOn#subscribeActual的逻辑是一样的,都
需要先获取一个Worker实例,然后将其与所传入的CoreSubscriber、
queueSupplier等一起包装为一个PublishOnSubscriber。对于代码中
传入的scheduler参数,我也不知道有什么用处,它的功能都由worker
实现了,因此其可能是进行代码迭代时未及时删除的冗余代码。
1.对PublishOnSubscriber的解读
接下来,一起来看看PublishOnSubscriber的实现,这也是本节的
核心。
为了适配worker进行调度,PublishOnSubscriber首先应该实现
Runnable 接 口 , 若 其 既 要 为 CoreSubscriber 又 要 为 Subscription 的
话 , 直 接 选 择 实 现 InnerOperator 接 口 即 可 。 那 么 为 什 么 还 要 实 现
QueueSubscription接口呢?下面就为大家慢慢道来。
我们会碰到状态感知型下发操作,即生产源无须暂存元素,根据
状态来进行元素下发的策略,比如Flux.generate。而为了配合这种情
况 , 首 先 会 看 到
reactor.core.publisher.FluxGenerate.GenerateSubscription , 先
行 实 现 QueueSubscription 接 口 , 并 对 int requestFusion ( int
requestedMode ) 方 法 进 行 实 现 。 在 前 面 的 章 节 中 已 经 分 析 过
QueueSubscription.requestFusion 的 催 化 效 应 , 但 是 如 何 让 其 与
publishOn操作进行衔接呢?对于订阅者来说,若想要与Subscription
沟通,订阅者只能从自己的onSubscribe方法中获取Subscription实例
并 调 用 它 的 request 方 法 , 那 么 下 面 来 看 看
reactor.core.publisher.FluxGenerate#subscribe的源码:
可以看到,在上面的源码中传入了actual这个CoreSubscriber,
其 onSubscribe 方 法 接 收 了 FluxGenerate.GenerateSubscription 对
象,也可以说接收了QueueSubscription类型的实例。当下游操作链中
需 要 执 行 异 步 化 切 换 线 程 的 操 作 时 , 就 将 publishOn 放 在
Flux.generate 之 后 , 那 么 <1> 处 会 变 成
FluxPublishOn.PublishOnSubscriber #
onSubscribe(GenerateSubscription)。下面来看看相应的源码:
根据之前所述,在Flux.generate生产源的场景下,上述源码中的
onSubscribe 传 入 的 参 数 s 代 表 GenerateSubscription 类 型 , 其 属 于
QueueSubscription , 即 这 里 的
f.requestFusion(Fuseable.ANY|Fuseable.THREAD_BARRIER)调用的
是 FluxGenerate.GenerateSubscription # requestFusion 实 现 。 在 这
里 , onSubscribe 中 的 requestFusion 参 数 传 入 的 是 7 , ( 7&1 ) !
=0&&(7&4)==0得到的结果是false,所以返回Fuseable.NONE(此处
强 调 一 下 , 对 于 FluxGenerate.GenerateSubscription #
requestFusion这个方法,即便其中的if判断成立,返回的结果也是
Fuseable.SYNC , 不 可 能 是 代 表 可 异 步 处 理 的 模 式 ) , 接 着 回 到
PublishOnSubscriber.onSubscribe,在知道拿到的是Fuseable.NONE
这个结果后,会跳过onSubscribe中的两个if语句块,得到之前定义的
有界队列,由下游传入的订阅者执行actual.onSubscribe(this)。
假 如 后 面 还 有 一 个 publishOn 操 作 , 那 么 就 执 行
PublishOnSubscriber#requestFusion,这里就是(7&2)!=0,得到
的 结 果 是 true , 也 就 是 得 到 了 ASYNC , 并 会 将 上 一 个
PublishOnSubscriber的outputFused设为true。
但 这 里 要 明 确 , publishOn 中 的 异 步 操 作 主 要 是 通 过
queue=queueSupplier.get得到的queue来设计实现的,通过这个queue
将上游源与下游订阅者分隔开来。
因为下发和请求操作依然是在异步线程里执行的,所以它们可能
会分布在多个线程中执行任务(如果该订阅关系支持一个生产者对应
多个订阅者,那么我们可能会使用一个拥有多个线程的自定义线程池
来做调度)。那么如何保证请求的单一性?这时可以将上游元素下发
到 这 个 queue 中 , 当 下 游 多 个 线 程 发 送 请 求 的 时 候 , 直 接 增 加
PublishOnSubscriber 中 定 义 的 REQUESTED 数 量 , 接 着 等 待
PublishOnSubscriber从这个queue中获取元素并下发给下游订阅者。
而对于publishOn操作的上游元素生产者来讲,它看到的仅仅是一个
PublishOnSubscriber类型的订阅者,只需要将元素下发到这个queue
中即可。
这里拿Flux.generate来举例,publishOn为产生请求会调用如下
源码:
多个线程同时发送请求,这样会造成多次调用slowPath(n)或者
fastPath。我们当前的场景是多次调用slowPath(n),也就是说,会
多次调用s=g.apply(s,this)且并不一定能保证hasValue=false。
按正常的情况进行设定,会产生The generator didn't call any of
the SynchronousSink method异常。那么应该如何避免异常呢?先不
用着急,将PublishOnSubscriber的执行流程走完。
当后面还有一个publishOn操作时,前面介绍过,其会执行上一个
publishOn 的 PublishOnSubscriber # requestFusion 操 作 , 并 得 到
ASYNC,在调用publishOn操作相关的onNext时:
因为sourceMode==ASYNC,所以会执行第一个if语句块,也就是并
不会下发任何元素,这是因为元素都由它前面的那个publishOn操作放
置在它自己的queue中了(我们的一个订阅关系中存在两个publishOn
操作),接着结合PublishOnSubscriber实现了QueueSubscription接
口,后面这个publishOn操作只需要在trySchedule中从队列中拉取元
素并下发即可,可以看到worker.schedule(this):
由前面的内容可知,PublishOnSubscriber实现了Runnable接口,
所以这里的源码中传入的是this,下面看看这个类的run方法实现:
我们还要考虑一个问题,即对于两个连续的publishOn操作,如果
第二个publishOn操作用于从队列中获取元素做下发操作,那么第一个
publishOn操作是不是只需往队列中存入元素,而无须做真实元素的调
度 、 下 发 工 作 ? 由 前 面 可 知 , 当 sourceMode==ASYNC 时 , 第 一 个
publishOn操作的outputFused=true,那么就会执行runBackfused。也
就是说,当一个调用链中出现多次publishOn操作的时候,后面的那一
个操作会通过QueueSubscription废弃前一个publishOn操作,虽然前
一个publishOn操作也会切换线程,但在线程任务里只会做一个衔接动
作(即actual.onNext(null)),而并不会做与真实下发元素相关的
其他任何事情。其实这样只是在玩切换线程的游戏:
再次回到Flux.generate与只有一个publishOn操作的关联上(如
果有多个操作的话,只看最后一个操作即可)。可以知道
FluxPublishOn.PublishOnSubscriber # onSubscribe 最 后 调 用 的 是
FluxGenerate.GenerateSubscription#request方法,然后其内部会
调 用 订 阅 者 的 onNext 方 法 来 进 行 元 素 的 下 发 。 而 由 前 面 所 展 示 的
FluxPublishOn.PublishOnSubscriber#onNext的源码可以知道,其首
先执行了queue.offer(t)操作,将元素添加到最初传入的那个有界
队列中,接着执行trySchedule(this,null,t),也就是又回到了
这 里 定 义 的 run 方 法 中 。 因 为 首 先 调 用 的 是
FluxGenerate.GenerateSubscription 中 的 requestFusion , 其 返 回 值
为Fuseable.NONE(值为0),所以sourceMode依然是其默认值0(无须
再次赋值,初始值就是0)。这就是说,在run方法内,只需要关注其
默认的runAsync方法即可:
之前有提到,这里的异步指的是队列queue中元素的获取、下发异
步(因为下发元素的消费在另一个线程中执行,可能产生拉取动作的
冲突,所以需要执行判断逻辑。而在同步的情况下,元素的生产和消
费都在一个线程中执行,也就无须这么做了),这是由
reactor.util.concurrent.SpscArrayQueue#poll操作实现的,该操
作内部是由原子类来保证操作安全的,所以完全可以适应多线程的并
发获取操作。
那么如何保证请求安全,而不会发生多个线程同时产生并发请求
的情况?这时可以通过原子类操作获取元素,然后下发,元素是根据
队列顺序一个一个获取的,它们并不会同时产生上面源码中的
e==limit(即消费元素个数达到publishOn操作一次请求上游元素个
数,默认是PublishOnSubscriber中所管理队列可存储元素数量),接
着通过原子类操作REQUESTED来保证元素请求数量的原子性。
因为队列中已经没有元素,所以要通过publishOn操作去上游拉取
元素。首先,将下游订阅者所需总元素数量减去这次要请求元素的数
量:r-e,然后执行s.request(e),此时e值是一次能请求元素数量
的 最 大 值 , 它 可 以 通 过
this.limit=Operators.unboundedOrLimit ( prefetch , lowTide ) 获
取,默认为PublishOnSubscriber中所管理队列可存储元素的数量,请
求完毕,再次将e设定为0。
这里会出现一个问题,当元素生产速度小于消费速度时,在
FluxPublishOn中出现的情况是暂存元素队列调用poll方法得到的是
null。我们要做的就是对调用该poll方法得到的元素是否是null进行
判断,即PublishOnSubscriber#runAsync方法中的empty=v==null,
如果empty为true,那么就跳出循环,等待下次下发元素。在下一个元
素 下 发 的 时 候 , 会 再 次 调 用 trySchedule 方 法 ( 参 考
FluxPublishOn.PublishOnSubscriber#onNext),如果我们所用调度
器底层执行者为多个线程的线程池,那么就会看到线程切换的情况。
当元素生产速度大于消费速度时,工作的线程会不断地从
FluxPublishOn的暂存队列中获取元素进行消费,这样就不会出现消费
线程反复切换的情况了,相关源码如下:

在这里,通过一个Demo来为大家对比上述两种情况:
若元素的生产速度没有消费速度快,则执行结果如下:
将Demo代码中的sleep(10);注释掉,再次运行代码,执行结果
如下:
两者对比,可以看到,差之毫厘,结果真的是失之千里。不仅元
素生产线程可能会发生变化,元素消费线程也会根据元素生产和消费
的速度差产生变化。希望大家多加思考,若不明白,可以多阅读几遍
源码。也希望大家可以通过学习这些原子类的用法,来让自己的代码
产生同样强烈的化学反应。
至此,可以看到,FluxPublishOn.PublishOnSubscriber通过实现
reactor.core.Fuseable.QueueSubscription 接 口 并 对 requestFusion
进行设定,做到了对sourceMode进行控制,以此可以应对后续多个
publishOn操作可能产生的问题,并对最后一个元素之前(不包括最后
一个元素)所重复的publishOn操作屏蔽绝大部分功能(具体前面已经
介 绍 得 很 清 楚 了 ) 。 从 FluxGenerate.GenerateSubscription 到
PublishOnSubscriber,可以感受到QueueSubscription带给我们的穿
针引线的效应,可以在多个类间进行状态控制。希望读者同样可以掌
握 : 一 个 接 口 ( QueueSubscription ) 、 一 个 本 地 状 态 ( 类 中 的
sourceMode)、一个链式调用方法(onSubscribe,后者根据前者的结
果来进行自己的行为设定)。

3.3 深入解读subscribeOn
为了方便理解,首先通过图3-2来展示subscribeOn操作的行为细
节。
图3-2
subscribeOn操作主要针对的是发生订阅的线程,也就是对生产初
始元素的线程的设定。注意,在图3-2中,在调用subscribe方法后会
切换线程。
subscribeOn 操 作 的 实 现 逻 辑 与 RxJava 2 中 的 Flowable #
subscribeOn(io.reactivex.Scheduler,boolean)的实现逻辑如出
一辙,与publishOn操作相比,其相对简单。在本节中,我们就来探其
究竟。
其实这里有一个比较有意思的“玩法”——请求异步化,这也是
开 发 中 容 易 忽 视 的 东 西 。 当 使 用 Flux # create ( Consumer ,
FluxSink.OverflowStrategy)作为源的时候,我们可以在生产元素的
过程中执行一些阻塞等待操作,也就是说,很可能造成元素的消费速
度大于生产速度。如下面的源码所示,在<1>处,请求可能会因元素生
产过程迟迟没有结束而产生阻塞,进而导致消费线程阻塞:
执行结果如下:
可以说,如果不注意,可能真的会防不胜防。为什么会发生这种
情形呢?下面慢慢道来。
在默认的情况下,subscribeOn传入的第2个参数为true:

直接看看subscribeOn操作的相关实现类:
由上面的源码可知,reactor.core.publisher.FluxSubscribeOn
#subscribe中的代码逻辑无非就是得到worker并进行调度。我们在使
用Flux#create方法时,首先会执行actual.onSubscribe,即下游会
调 用 上 游 Subscription 中 的 request 方 法 。 结 合 前 面 在
FluxSubscribeOn 的 subscribe 中 看 到 的 代 码 逻 辑 , 首 先 调 用
SubscribeOnSubscriber的request:
此时S.get(this)为空,因为还没有与上游FluxCreate产生交
互,也就拿不到Subscription,所以此时SubscribeOnSubscriber#
request并未做什么事情。接着,执行worker.schedule(parent),
此时会执行SubscribeOnSubscriber#run方法,上游源和下游产生订
阅,接着就会调用SubscribeOnSubscriber#onSubscribe中的代码逻
辑。为方便大家理解,下面将flux_subscribeOn这个Demo的相关调用
过程通过图3-3来进行展示。
图3-3
下面来看看SubscribeOnSubscriber中涉及的相关方法:
由 前 面 可 知 , 在 默 认 的 情 况 下 , requestOnSeparateThread 为
true,也就是说当下游没有publishOn这种切换线程的操作,而且恰好
中间产生了拉取元素的请求时,生产元素的过程有阻塞,即生产元素
的速度小于消费元素的速度。根据之前学习的内容可以知道,若产生
拉取元素的请求始终都与元素生产任务处于同一个线程中,并不会发
生阻塞,一切都按顺序执行。
而当上述情况中有publishOn这种切换线程的操作的时候,那么根
据之前学习的内容可以知道:
◎ 在产生订阅并发起拉取元素请求的时候,该请求与元素生产任
务 处 于 同 一 个 线 程 , 参 考 图 3-3 , 此 处 设 定 为 A ( 由
subscribeOn确定,参考上面源码片段中的run方法的实现,
其内顺带设定了刚开始生产元素所在的线程THREAD,当然,
也会在这个生产线程内执行onSubscribe)。
◎ 而当publishOn操作需要请求时,该请求发生在元素消费任务
所 在 的 线 程 中 , 设 定 为 B , 该 请 求 调 用 的 是
SubscribeOnSubscriber # request , 也 就 是 会 调 用
requestUpstream ( n , s ) 方 法 , 此 时
Thread.currentThread ( ) ==THREAD.get ( this ) 返 回
false , 即 在 requestUpstream 方 法 中 会 调 用
worker.schedule(()->s.request(n))。
在 这 里 , 需 要 注 意 的 前 提 是 , 需 要 将 subscribeOn 操 作 放 在
publishOn操作之前。也就是worker中提交了一个任务,假如所提交的
线程池中只有一个线程,任务就会进入排队状态(由前面对elastic的
分析可知,其获取的是一个只包含一个线程的线程池,elastic所针对
的并发是指多个发布-订阅关系的并发操作,而不是单个订阅关系内的
并发操作,对于单个订阅关系内的并发操作,我们可以自行定义一个
线程池,如Demo所示),这就是说,只有在彻底生产完元素之后,请
求才会执行,也才会消费接下来的元素。假如是无限元素生产源或者
元素生产量非常大,那么就永远别指望执行后续的操作了(针对无限
元素生产源来讲),直到发生OOM。即便元素生产量不足以发生OOM,
但因生产元素消耗了大量时间,其响应速度同样也会大大降低。

下面再来看一个Demo:
代码执行结果如下。结果完全符合预期,publishOn操作会每隔2
个元素请求一次,具体原因之前介绍过,不明白的读者可以回顾一下
对elastic的解读。产生的request任务会在subscribeOn所在的线程中
提交。
要解决这种情况下产生的阻塞异常,就要将请求任务执行线程和
元素生产任务线程分开(即让它们不在同一个线程中)。我们只需要
在subscribeOn中指定第2个参数为false即可,这样requestUpstream
中 的 if ( ! requestOnSeparateThread||Thread.currentThread ( )
==THREAD.get(this))直接成立,在元素消费任务线程中直接进行
请 求 , 最 后 会 调 用
reactor.core.publisher.FluxCreate.BufferAsyncSink # drain 方
法,我们对flux_subscribeOn()这个测试用例进行如下修改(注意
看标记的重点):

执行结果如下:
最后需要说明的是,因为订阅只会产生一次,也就是说
source.subscribe(this)只会执行一次,所以假如执行链中存在多
个subscribeOn方法,那么只有第一个有效果,其他只是执行正常的元
素下发操作。
3.4 Flux.parallel&Flowable.parallel的并行玩法
学完前面的内容可以知道,之前所接触的调度操作都会保证元素
消费的先后顺序,那么有没有一种方法可以做到类似于JDK中的Stream
那样的并行操作呢?Stream中对并行操作的应用场景有一个很重要的
要求,那就是元素消费没有先后顺序。
下面来看一张示意图,如图3-4所示。

图3-4
图3-4中的操作思路其实分为如下两步。
◎ 第1步,调用Flux.parallel(int parallelism)来将数据元
素根据指定的参数parallelism分割成多个与数量相匹配的
组。
◎ 第2步,为每个组都匹配一个订阅者,将同一个源生产的元素
按一定规则分配给每一个订阅者,也就是对同一个源生产的
元素按规律分割,然后产生等同于parallelism个生产-订阅
关系。最后,针对每一个生产-订阅关系,分别使用调度器来
完成这种多任务并行操作。
Flux.parallel与RxJava 2中的Flowable.parallel(注意,推荐
使用RxJava 2.1以上的版本,2.0.5版本仅为实验版,不推荐使用)的
实现逻辑一样,下面只分析Reactor 3中的实现过程,读者若对RxJava
2中的实现细节感兴趣,可自行对比分析。
下面来看Flux.parallel的实现细节:

从上面的源码可以知道,在默认的情况下,指定的parallelism为
可用的处理器数量。假如本机CPU支持超线程,那么根据开启超线程后
的CPU线程总量进行计算。最后,调用Flux中的一个静态方法,请注意
得到的结果类型为ParallelFlux,它是Publisher的另一种实现。其大
部分内容一看便懂,我们主要会查看其子类ParallelSource:
在这里,请注意subscribers是一个数组,后面会详细讲解此数组
的构造,先来看看validate(subscribers)的验证逻辑:
上面的代码中用到了parallelism方法,另外validate方法定义在
其 父 类 ParallelFlux 内 , 因 此 从 ParallelSource 这 个 类 将 看 不 到
parallelism的使用痕迹,在此特别指出。validate方法主要用于对分
配的订阅者数组长度和并行处理的数量进行比较判断,结合Flux#
parallel方法可知,在默认的情况下,在对订阅者数组进行初始化的
时候,其中包装了parallelism个LambdaSubscriber,下面来看看其实
现:
根据指定的参数创建大小为parallelism数量的LambdaSubscriber
数组,并将此数组作为参数传入subscribe(CoreSubscriber<?super
T>[])中。
接着,可以知道,参与订阅后,首先调用的是订阅者的
onSubscribe方法:

可以看到上面源码的主要逻辑。首先是执行setupSubscribers方
法,然后会对上游源发出拉取元素请求,将该请求标记为A,而在
setupSubscribers方法中,那些产生订阅关系的拉取元素请求和这个A
请求是分开的,也就是说s.request请求的是主源,子Subscriber请求
的是按策略分下去的元素分发者ParallelSourceInner,所以在这里该
SourceInner充当的角色是Subscription:

分 别 调 用 每 一 个 订 阅 者 的 onSubscribe ( new
ParallelSourceInner<>(this,j,m))方法,根据我们所知,在调
用onSubscribe时,主要会执行其中的Subscription#request方法,
下面就来对ParallelSourceInner中的request实现进行分析:
首先对每一个subscriber的元素请求数量进行设定。这里通过主
订阅者ParallelSourceMain中AtomicLongArray类型的requests数组,
对各个子订阅者进行元素请求数量的管理,即通过原子类操作一一对
requests数组中对应索引位置的数量进行设定。在完成最后一个设定
时 , 即 parent.subscriberCount==length , 就 进 入 parent.drain 逻
辑。
请 求 后 就 需 要 下 发 元 素 了 , 接 着 看 看
ParallelSource.ParallelSourceMain#onNext方法:
与之前接触过的publishOn有点像,会先往队列中添加下发元素,
不同的是这里执行的是drain方法,而publishOn执行的是任务的调度
方法trySchedule。而drain又和publishOn中的PublishOnSubscriber
#run方法有些相似(这里只是举个例子,其实还有其他相似的方
法):
drainAsync用于执行队列queue中元素的获取下发,其所做的动作
与FluxPublishOn.PublishOnSubscriber#runAsync的代码逻辑比较一
致,只不过为了适配下发策略,实现上稍有不同,下面看看这部分的
主要实现源码:
下面对这个过程进行分析。
◎ 在这里,通过AtomicLongArray r=this.requests;来进行原
子类中的数组控制,数组的长度等于subscribers.length,
this.requests设定了每一个订阅者的元素请求数量。
◎ 接着,在ridx=r.get(idx)中,只要idx不超出临界值,ridx
获取到的值就为对应subscriber的元素请求数量。
◎ 由int idx=index;可知,index字段的默认值为0,那么idx的
初始值也为0。同样,在long[] e=this.emissions;中,由
于 this.emissions 的 初 始 值 是 一 个 空 数 组 , 因 此 long
eidx=e[idx];的初始值也是0。假如ridx!=eidx条件成立,
从队列中获取元素,接着执行a[idx].onNext(v)下发元
素,其中idx就是用来确定subscribers数组中下标为idx的订
阅者所在的线程什么时候可以消费元素的策略,最后执行
idx++。
◎ 当idx达到n=e.length临界值时,idx归0,回到subscribers中
第一个子订阅者,如此这样,通过不断循环轮询的方式,每
一个订阅者都可以获取属于自己的要消费的元素,当
c==limit的时候,再次发起请求即可。这就是并发情况下所
执行的下发策略。
◎ 在这个过程中,如果对元素请求数量进行消费控制,会怎么样
呢?可以看到,在每下发一个元素时,就会执行
e[idx]=eidx+1,简化一下代码,变为eidx++,然后执行下一
次循环来比较ridx!=eidx,如果条件语句成立,则继续下发
元素。也就是说,这些子订阅者共用了同一个元素请求数
量,一旦元素消耗完毕,就停止下发操作。
需要记住的是,每一个订阅者都可能有publishOn操作的增强包
装,以完成并发消费调度。为了更易于操作,下一节会涉及
ParallelFlux.runOn方法细节,实现类似的调度功能。
同时,我们还要知道this.requests原子类对象中的值如何设定。
设定过程隐藏在针对subscribers数组中每一个subscriber的轮转顺序
进 行 设 定 的 类
ParallelSource.ParallelSourceMain.ParallelSourceInner中。其设
计理念主要基于某件事到底该交由谁来处理,下游订阅者的元素请求
数量消耗完毕,如果要增加元素请求数量,就应该将this.requests中
每一个子订阅者的元素请求数量进行统一更新。因为真实情况是,子
订阅者才会真正地消费元素,即由我们定义消费动作的指定订阅者包
装而来,元素请求数量是共有的。希望读者可以细细品味其中的设计
技巧。

3.5 ParallelFlux.runOn&ParallelFlowable.runOn
的调度实现
为了适配多个订阅者的情况,调度也做了相应的改变:主要是给
每一个订阅者都分配一个FluxPublishOn.PublishOnSubscriber来做调
度操作。由上一节可知,从整体上看,订阅动作依然只是发生一次
( 与 ParallelSourceMain 产 生 订 阅 ) , 其 内 是 通 过 drain 方 法 针 对
subscribers数组中的子subscriber来进行元素的下发操作。对于元
素,如果想要进行并行消费,结合上一节最后所介绍的,应该采用
PublishOnSubscriber。
同样,RxJava 2和Reactor 3中的实现逻辑大致一样,下面只分析
Reactor 3中的相关实现:
可 以 看 到 , 通 过 for 循 环 对 每 一 个 订 阅 者 使 用
PublishOnSubscriber进行增强包装,使其具备调度能力,再参与订
阅。在使用PublishOnSubscriber执行调度操作时要注意,当下发元素
时 , 会 先 将 元 素 存 放 到 一 个 queue 中 , 由
subscribers[i].onSubscribe(new ParallelSourceInner<>(this,
j , m ) ) 可 知 , 每 一 个 下 游 订 阅 者 接 收 到 的 Subscription 都 为
ParallelSourceInner , 而 ParallelSourceInner 并 没 有 实 现
QueueSubscription接口,所以也就无须考虑上游是否有策略来通过公
用队列利用PublishOnSubscriber#onSubscribe方法限制下游runOn消
费元素的并行调度。
此处,再给大家展示一下PublishOnSubscriber#onSubscribe的
相关源码,以方便大家进行对比思考:
最后,通过一个小Demo来演示上面介绍的内容:

执行结果如下:
其实,关于ParallelFlux,其中还有一些比较好玩的API。比如,
若想要分组,自然就会想到通过一个ParallelFlux#groups操作执
行,返回的结果类型是Flux<GroupedFlux<Integer,T>>。这其实就是
利用flatMap操作结合ParallelSource来并行执行分组操作,所以,这
里就要注意了,我们可以直接调用ParallelFlux#composeGroup方法
来 达 到 这 个 目 的 ( 在 Reactor 3.4.0 之 后 , 该 操 作 名 字 迭 代 更 改 为
transformGroups),感兴趣的读者可以去探索具体源码。

3.6 小结
至此,关于调度器的内容就告一段落了。本章讲解了很多调度器
源码级的实现,以及其各种各样的内在问题,其中也包括一些值得借
鉴的实现方法。另外,本书还提供了更深入的视频版源码解读分享,
感兴趣的读者可以结合起来一起学习,以获得更多的收获。
接下来,会接触到一些Reactor 3中的操作,其实现逻辑与RxJava
2中的一样,我们在《Java编程方法论:响应式RxJava与代码设计实
战》一书中通过Observable对RxJava 2中很多操作的源码进行过分
析,大部分操作的思路并没有太大的变化,因此就不再花篇幅在其源
码解读上了,而对于一些没有涉及过的内容,我会带着大家一起体验
一下,这也是对RxJava 2相关源码解读内容的补充。
第4章 对Reactor操作的解读
通过使用Operator可以得到一些好处,如代码可重用,代码干净
整洁,更加专注于业务实现等。带着这个观点,本章将讲解一些常用
的Operator操作。对于这些操作的具体源码细节,如果读者感兴趣,
可从本书配套视频中查找对应的解读视频。

4.1 filter操作
下面来看看filter操作的行为示意图,如图4-1所示。

图4-1
如 图 4-1 所 示 , 对 源 中 下 发 的 元 素 进 行 过 滤 , 只 留 下 满 足
Predicate指定条件的元素。如下面的Demo所示,只输出1到10中的所
有偶数:

4.2 transform操作
下面来看看transform操作的行为示意图,如图4-2所示

图4-2
该操作用来将上游Flux通过一定的自定义逻辑转换并生成想要得
到的目标Flux。如果图4-2中flux->flux.take(v)里的v为1,则只获
取第1个元素,执行完操作就结束下发元素。为了更好地阐述该操作的
用法,直接看一个例子:

transform操作与RxJava 2中的compose操作的作用是一样的,只
是两者实现上有细微的差别,Reactor中的实现更加简单、粗暴,下面
来进行对比。
RxJava 中 io.reactivex.Flowable # compose 的 参 数 传 入 类 型 为
FlowableTransformer,此类型的接口定义如下:
在《Java编程方法论:响应式RxJava与代码设计实战》一书中有
介绍过io.reactivex.Observable#compose的实现,其所传参数类型
ObservableTransformer的接口定义如下:

可以看到,上述两者除了参数类型和返回类型不一样外,结构定
义完全一样,为什么不能都统一呢?下面在Reactor中基于JDK 8来做
一些迭代改进,通过java.util.function.Function接口来使其统一:

接着,有如下实现:

对比一下RxJava中的实现:

其中的异同,已然很明显了。在Reactor 3中,通过transform操
作,可以在一个方法中封装一系列操作,然后将此方法作为参数传
入。在这里,为了抽取通用方法而做了一些工作,尽量避免重复的代
码逻辑。
下面来看如下源码:

通过图4-3来展示上述源码的执行过程。

图4-3
图4-3对transform操作在上面Demo中的执行过程进行了清晰的解
读,其仅仅实现了对filter和map两个操作的抽取。这里的Stateful指
的是,调用包装订阅者的onNext所下发的元素类型或值与上游源下发
的元素类型或值相比有所不同。
每一个中间操作主要实现了两个功能:对源的传递与包装所传订
阅者并使其具有特定的能力,只有在真正产生订阅的时候,才会对订
阅者进行层层包装。所以对于图4-3中的3种颜色(这里可将订阅者
lambdaSubscriber看作蓝色,其他两种颜色是白色和黑色),应该清
楚其代表的含义和在实际应用中应该注意的地方。
执行结果如下:

4.3 compose与transformDeferred操作
由 上 一 节 可 知 , 在 执 行 transform 操 作 时 , 其 实 是 在 执 行
from ( transformer.apply ( this ) 。 可 以 看 到 , 这 里 直 接 执 行 了
transformer这个Function对象所定义的代码逻辑,但有时候会想要在
产生订阅时再执行这个代码逻辑,即延迟执行这个代码逻辑,那么可
以使用一个Supplier来实现:()->transformer.apply(this),然
后 将 Flux.from ( ... ) 换 作 Flux.defer ( ... ) , 于 是 就 实 现 了
compose操作。
由于compose名字本身体现不出延时特性,所以在Reactor 3.4.0
版本之后,使用transformDeferred(Function)对其进行了改名,这
样语义更加明确。从此compose这个API退出了历史舞台,但它的内部
实现并没有发生变化。从封装的角度来讲,transformDeferred就是将
通用操作放在一个包装过的subscriber中执行,其在编译的时候并不
会 替 换 transformer , 只 有 在 真 正 产 生 订 阅 关 系 的 时 候 , 才 会 执 行
transformer这个Function对象所定义的代码逻辑。具体源码如下:
从Subscriber的角度来看,transformDeferred(...)中封装的
操作属于一个黑盒状态的可变的操作,而从Publisher的角度来看,各
个操作返回的源是Stateless。下面看看如下源码:
在上面的源码中,通过一个原子类状态进行控制,可以得到不同
的结果,也就是说当我们包装的通用操作可以选择策略的时候,可以
选择transform操作。若在产生具体订阅时才会发生原子类的相关状态
变化,那么在这种场景下必须选择transformDeferred操作(老版本中
可以使用compose操作)。
通过图4-4对上面的源码进行解读,来带给大家一个更直观的感
受。
图4-4
执行结果如下:
4.4 批处理操作
当你拥有很多元素,而又想将它们分割成不同的批次进行处理
时,在Reactor中有3种操作可供你选择:buffer、window和groupBy。
这3种操作的方法很接近,它们都是将一个Flux<T>源重新分配到一个
容 器 中 , 其 中 window 和 groupBy 操 作 创 建 了 一 个 Flux<Flux<T>> , 而
buffer操作将元素收集到一个Collection<T>中。
4.4.1 buffer操作
首先通过图4-5来看看buffer的行为操作。

图4-5
buffer操作代表了一系列关于FluxBufferXXX封装的操作。buffer
和bufferTimeout(如图4-6所示)的作用是把当前源中的元素收集到
集合(默认是ArrayList)中,并把集合对象作为流中的新元素下发。
在进行元素的收集时,可以指定不同的条件,如list中包含的元素的
最大数量或收集的时间间隔等。比如,buffer方法可以指定list所存
储元素的最大数量maxSize,以及要跳过的前skip个元素,并且buffer
方法也提供了函数表达式bufferSupplier。
在这里,通过图4-6来展示bufferTimeout的行为操作细节。
图4-6
除 了 buffer 和 bufferTimeout 外 , 还 可 以 通 过 bufferUntil 和
bufferWhile操作来进行元素的收集。这两个操作的参数都是表示每个
集合中的元素要满足的条件的Predicate对象。bufferUntil会一直收
集元素,直到Predicate返回true。可以将使Predicate返回true的那
个元素选择、添加到当前集合或下一个集合中;bufferWhile则只有在
Predicate返回true时才会收集元素。一旦其值为false,会立即开始
执行下一次收集操作。
看看下面的Demo。第1行语句会输出5个包含20个元素的数组。第2
行语句会输出5个包含2个元素的数组,每当遇到偶数时就会结束当前
的收集操作。第3行语句会输出5个包含1个元素的数组,数组里面包含
的只有偶数。第4行语句会输出2个包含10个元素的数组,在这里,每
隔1s产生1个元素,将10s内下发的元素作为一个ArrayList下发。
执行结果如下:
需要注意的是,首先通过toStream方法把Flux源转换成JDK 8中的
Stream对象,然后通过forEach方法来进行输出。这是因为interval的
元素产生采用了调度器,所以是异步的,而转换成Stream对象可以保
证主线程在产生完元素之前不会退出,从而可以正确地输出源中的所
有元素。
4.4.2 window操作
为了方便理解,首先通过图4-7来展示window操作的行为细节。

图4-7
window操作的作用类似于buffer操作,不同的是window操作是把
当 前 源 中 的 元 素 收 集 到 其 他 Flux 源 序 列 中 , 因 此 返 回 值 类 型 是
Flux<Flux<T>> 。 这 里 的 参 数 比 较 有 意 思 , 对 于 window ( int
maxSize),其默认设定的是Queues.get(maxSize)的队列,假如这
个 maxSize<=10_000_000 , 则 该 队 列 就 是 一 个 有 界 队 列 。 而 对 于
window(int maxSize,int skip),其默认设定的是无界队列。假如
读者有心的话,对比不同版本的Reactor,其实可以发现API的参数设
定是有差异的,希望大家在阅读本书时以Reactor 3版本为参考,这也
是我在本书中一直强调使用RxJava 2(而不是RxJava 1)和Reactor 3
版本的原因。另外,在Reactor 3版本范围内的小版本之间,参数设定
也有差异。
和buffer操作一样,window操作也有一系列相关联的操作,如
window、windowTimeout、windowUntil、windowWhile及windowWhen操
作等。
与我们后面会讲到的groupBy操作进行对比,window操作往往是序
列化的(即顺序化的)。可以认为这里的window是队列,因为这里的
源是以队列为基础的UnicastProcessor创建的,其中一个子源会被简
称为一个window,比较形象。而groupBy操作会根据策略进行分组,分
组类似于集合。
对于window操作,由于它会将接收的下发元素缓存在它管理的队
列中,因此也可以认为其是一个集合,一个集合在达到条件所指定的
最大值后会重新创建另一个集合,并将上一个window置空,所以最多
存在两个集合,具体实现可以深入源码进行分析,其背后的实现思路
和buffer操作的类似,只不过buffer操作的默认list集合在这里变成
了披着Processor外套的队列。
下面来看一个简单的Demo:
执行结果如下:

从执行结果可以看到,前两行分别是两个UnicastProcessor字
符。这是因为window操作所产生的源中包含的是UnicastProcessor类
的 对 象 , 而 UnicastProcessor 类 的 toString 方 法 输 出 的 是
UnicastProcessor字符串,其他类似输出也是同样的。
还有一点很重要,对于window(int maxSize,int skip),其中
的skip与buffer(int maxSize,int skip)中的skip并非表示该单词
表面意思上的跳过,我们可以将其理解为创建一个新集合的契机,但
并不会直接抛弃老集合,接下来一起探索其中有意思的源码:
根据上面的源码,下面分4个部分来进行讲解。
◎ 在<1>处,当index==0时,创建了一个新集合,这里的表现就
是 获 取 一 个 UnicastProcessor 对 象 , 并 将 其 加 入 当 前 作 为
ArrayDeque角色的WindowXxxSubscriber。
◎ 在 <2> 处 , 会 遍 历 作 为 ArrayDeque 角 色 的
WindowXxxSubscriber , 并 调 用 其 中 的 UnicastProcessor 的
onNext方法对元素进行下发。此处比较关键,其中的所有
UnicastProcessor对象(虽然最多只能有两个该对象,具体
可查看第<3>部分)都会对同一元素进行下发。
◎ 在<3>处,当生产元素的数量达到指定的size值的时候,就将
第 一 个 加 入 ArrayDeque 的 UnicastProcessor 移 除 ,
ArrayDeque的poll方法调用的是pollFirst,判断其返回值是
否为null,不为null的话,为这个源产生一个结束事件。
◎ 在<4>处,当i==skip(即index==skip)时,设定index为0,
这样下次就会再次创建一个新的UnicastProcessor对象加入
ArrayDeque中。
注意:由上面的这些过程可以知道,当size==skip的时候,刚好
ArrayDeque中只会存在一个UnicastProcessor对象;当size<skip的时
候,ArrayDeque中会有失去UnicastProcessor对象的情况,当p==size
时,会移除UnicastProcessor对象,但因为size<skip,所以并不会产
生 新 的 UnicastProcessor 对 象 , 也 就 是 到 下 一 次 产 生 新 的
UnicastProcessor对象之前,期间的元素都会被丢弃;当size>skip的
时候,会执行上面的正常步骤,会产生同一个元素在两个
UnicastProcessor对象中同时存在的情况。
下面通过图4-8到图4-10来更直观地展示以上过程。
当size<skip的时候:
图4-8
当size==skip的时候:
图4-9
当size>skip的时候:
图4-10
对于buffer和window操作的区别,下面只用size<skip时的图来体
现,主要还是在元素的下发形式上有所区别,如图4-11所示。
图4-11
接下来使用一个Demo来展示上面所涉及的内容:

执行结果如下:

4.4.3 groupBy操作
为了方便理解,下面通过图4-12来展示groupBy操作的行为细节。
图4-12
对于groupBy操作,我们已经在《Java编程方法论:响应式RxJava
与代码设计实战》一书中介绍过,大家可以参考该书中的内容了解具
体实现细节,或者观看本书的配套视频。下面只带大家进行一下回
顾。
groupBy操作会通过一个策略key将一个Flux<T>分割为多个用于批
处理的组,下面来看看其定义:

从这里可以看到,分割出来的每一个组都是GroupedFlux<T>类型
的元素,其可以根据keyMapper中的K来获取。
与buffer和window操作不同,groupBy操作过程的特性就注定了在
它的这些组中的元素没有顺序,因为组的产生是根据元素所分配的组
是否存在来决定的,不存在就创建组,后续的元素则根据匹配策略添
加到相应的组中,所以组的创建没有什么绝对顺序,完全由源中的元
素决定,而且创建的几个组可以同时存在。对比图4-12,最下面一行
箭头代表方块元素,倒数第2行箭头代表圆形元素,倒数第3行箭头代
表分组时间点,这里是按所传元素形状进行分组的。
由此,可以知道组应该包含如下一些特性。
◎ 几个组之间的元素没有交集,也就是一个元素只属于其中一个
组。
◎ 组中的元素来源于原始源序列中不同的位置。
◎ 组永远不会为空,因为下发元素进行分组的时候发现没有组才
会创建组。
最后,通过一个Demo来展示groupBy操作的用法:

执行结果如下:

4.5 merge和mergeSequential操作
为了方便理解,下面通过图4-13来展示merge操作的行为细节。
图4-13
merge和mergeSequential操作都会将多个源合并成一个Flux源。
两个操作的不同之处在于,merge操作按照所有源中元素的实际产生顺
序进行合并,而mergeSequential操作则按照所有源被订阅的顺序,以
源为单位进行合并。
前者较好理解,下面通过一个示意图帮助大家理解后者,如图4-
14所示。

图4-14
mergeSequential操作内部的每个源都会单独产生一个订阅关系,
然后将生产出的元素下发到同一个队列中,整个过程中所有的源都是
按顺序依次执行的(即源内部依次产生订阅下发元素到同一个队列
中,上一个源的所有元素下发完毕后才会开始下一个源中的操作)。
下面来看一个Demo:

执行结果如下:

4.6 flatMap和flatMapSequential操作
flatMap和flatMapSequential操作用于从源中的每个元素所指向
的子源中得到其中的所有元素,再把所有源中的元素进行合并。
flatMapSequential 和 flatMap 操 作 之 间 的 区 别 与 mergeSequential 和
merge操作之间的区别是一样的。
下面通过两个示意图来更形象地展示这两个操作,如图4-15和图
4-16所示。
关于flatMap操作:

图4-15
关于flatMapSequential操作:
图4-16
最后,用一个Demo来对这两个API进行对比:
执行结果如下(读者可自行与上面的图进行对比):

4.7 concatMap操作
下面通过一个示意图来对concatMap操作进行展示,如图4-17所
示。

图4-17
concatMap操作也是根据源中的每个元素来获取一个子源,再把所
有子源进行合并的。与flatMap操作不同的是,concatMap操作会根据
初始源中的元素顺序依次将获取到的子源进行合并。与
flatMapSequential操作不同的是,concatMap操作对所获取到的子源
的订阅是动态进行的,而flatMapSequential操作在合并之前就已经订
阅了由初始源下发元素所得到的所有的子源(不会等前一个子源下发
完毕后再进行下一个子源的订阅)。
下面通过表4-1来对比3个操作。
表4-1
最后,通过一个Demo来展示concatMap操作的用法,若大家对具体
源码感兴趣,还可以继续深入:

执行结果如下:

4.8 combineLatest操作
combineLatest操作会把所有源中最新产生的元素合并成一个新的
元素下发。只要其中任何一个源中产生了新元素,合并操作就会执行
一次,然后下发新产生的元素,如图4-18所示。

图4-18
之前在《Java编程方法论:响应式RxJava与代码设计实战》一书
中介绍过,有多个源发出元素,针对其中一个源来说,其发出的某一
个元素只寻找离自己最近发出的元素进行合并,大家可以观察一下图
4-18。combineLatest操作的实现逻辑与RxJava中的相同,这里就不再
赘述了。
下面通过一个Demo来进行展示:

执行结果如下:
4.9 ConnectableFlux的二三事及对reactor-bug的分

在《Java编程方法论:响应式RxJava与代码设计实战》一书中,
已经对ConnectableObservable进行了详细解读,包括其中涉及的操作
细节,而本节将通过Reactor中的类似操作来进行回顾。
有时候,你不仅希望在一个订阅者和源产生订阅后推迟它整个订
阅逻辑的执行时间,同时也希望在订阅者达到一定数量或激活一个策
略条件的时候才触发真正的订阅操作和数据/元素的产生和下发。这也
是 为 什 么 需 要 ConnectableFlux 的 原 因 。 而 Flux 中 涉 及 的
ConnectableFlux返回值类型主要有两个,即publish和replay。
◎ publish:当源面对来自订阅它的各个订阅者的请求时,这里
就背压而言,通过将这些请求转发给源来进行动态的处理。
此处需要注意,如果其中任何一个订阅者的待请求元素数量
变为0,那么源会暂停所有元素的下发或拉取操作。
◎ replay:在第一次订阅的时候产生数据,并将其缓存起来,缓
存大小可自行定义(该缓存在replay操作内部是一个队列,
可以是有界的,也可以是无界的,根据个人需求定义)。注
意,此处假如是有界队列,只缓存最近的数据,然后可以将
这些数据下发给后续的订阅者。
关于replay操作,当有订阅者第一次与通过replay操作得到的源
产 生 真 正 的 订 阅 时 ( 通 过 调 用 ConnectableFlux 的 connect 方 法 实
现),会先将源下发的元素缓存起来,然后下发给订阅者,若后续又
有订阅者添加进来,则直接下发缓存中存储的元素,如图4-19所示。

图4-19
下面通过这个Demo对上面介绍的publish和replay操作进行展示:
执行结果如下:
可以看到,前两个订阅者在执行co.connect();前并不会真正
产生订阅,第3个订阅者是在真正产生订阅之后加进来的。为什么会产
生这种效果?这是因为在调用co.subscribe的时候,会将这个订阅者
加入一个订阅者集合中,然后在下发元素时依次对这个集合中的订阅
者遍历下发。
接着将<1>、<3>处的代码注释掉,并将<2>、<4>处的代码打开
(删掉前面的注释符号),这次的执行结果如下:
在这里,只设定了暂存两个元素。为了提升效果,将
Thread.sleep时间延长到10000ms,执行代码后发现,输出了两个连续
的Three,表示缓存了两个元素。
从co.connect也引出了另一个话题,ConnectableFlux提供了一些
其他方法来管理这些下游订阅关系与上下游的联系。下面看看其中涉
及的方法。
◎ connect:在已有订阅关系下,在你认为合适的时机,就可以
调用这个方法,用来触发产生内部真正的订阅关系。
◎ autoConnect(n):与connect方法的作用一样,只是添加了
n
一个自动触发的条件,当订阅者数量达到 时,就触发真正的
connect操作。
◎ refCount(n):这个方法不仅可以用于跟踪接入的订阅关
系,还可以用于跟踪这些订阅关系的解除状况。在这个过程
中,只要产生的订阅关系数达到 ,就会触发connect操作。 n
在连接期间,假如有订阅者取消了订阅关系,而且现存订阅
n
者数不够 个,这时并不会断开连接,而后续有订阅者加入并
且有订阅关系的订阅者数达到 个的时候,则会立马断开连 n
接。此处是Reactor的一处Bug,Reactor开发者自己也说这里
处于混沌中。后面我会分析一下该Bug的问题所在,仅针对本
书使用的Reactor版本,即3.1.7版本(在本书写作时使用的
是当时最新版本的Reactor,其中开发者并未对此Bug进行修
复)。
读 者 需 要 将 refCount ( int , Duration ) 和 refCount ( n ) 区 分
开,两者在实现上有很大的区别,在订阅者数满足 个的条件后,两者 n
都会进行正常的连接,使用refCount(int,Duration),在订阅者取
消订阅,真正断开连接之前,会等待Duration的时间,以方便后续新
的订阅者可以补充进来,然后继续下发元素,这时不会再关心订阅者
n
数 是 否 达 到 。 在 我 看 来 , refCount ( int , Duration ) 算 是 针 对
refCount(n)的一个补丁形式的API。
在对上面说的Bug进行解读之前,先来还原一下Bug产生的场景:
执行结果如下:
可 以 看 到 , 在 正 常 执 行 3 次 之 后 , 也 就 是 在 两 次
Thread.sleep(3000)之间产生的下发元素刚好是3个(每秒下发1个
元素),接下来One取消订阅,但依然产生了元素,然后Four补充上
来,连接了一次,但同时带来了不好的消息,即彻底结束了订阅。这
时下发了3个异常事件(这也证明Four确实发出了订阅请求)。这种情
况不符合refCount(n)这个API的定义,下面就来解读一下为什么会
出现这种情况。
在 这 里 , 先 调 用 reactor.core.publisher.Flux #
publish(int):

其 返 回 了 ConnectableFlux 类 型 的 实 例 , 然 后 调 用
ConnectableFlux#refCount(int)方法:

接下来产生订阅关系,执行FluxRefCount#subscribe方法:
由上面的源码可知,最终调用的是RefCountMonitor.subscribe:
此处可以看到代码作者的思考,他自己也认为这里有问题,后续
会慢慢道出问题所在。乍一看,这里的代码逻辑很正常,产生一个订
阅 关 系 后 subCount 就 加 1 , 然 后 将 其 加 入 订 阅 队 列 中 , 接 着 当
subCount==n 时 , 调 用 真 正 的 connect 方 法 。 在 这 里 , 可 以 看 到
parent.source为ConnectableFlux实例,其内部的connect实现是抽象
的,这也是FluxPublish的具体实现。
下面再来回顾一下Flux#subscribe方法:

可 以 看 到 , 此 处 的 Disposable 对 象 其 实 就 是 我 们 定 义 的
subscriber,所以其中一个解除订阅后,另两个subscriber依然会继
续接收元素并下发,为什么这么说呢?可以看看LambdaSubscriber的
dispose方法:

上 面 的 源 码 中 调 用 了 Subscription 的 cancel 方 法 , 这 里 的
PubSubInner会作为LambdaSubscriber的Subscription参数传入,所调
用 的 相 应 的 cancel 方 法 实 现 如 下 ( 之 前 我 们 有 看 到
parent.source.subscribe ( inner ) , 而 PubSubInner 又 二 次 包 装 了
RefCountInner , 由 此 可 以 得 知 , s.cancel 调 用 的 是
FluxPublish.PubSubInner#cancel方法):
可以看到,这里仅仅是从PublishSubscriber管理的subscribers
中移除了当前这个subscriber,并不会影响其他数组中的订阅者。而
且RefCountMonitor中的SUBSCRIBERS字段的值会减1。至此都算正常。
接下来说到的问题,我们在自己的代码中也很可能会考虑不周。
由 RefCountMonitor.subscribe 可 知 , 当 subCount==n 时 , 会 发 生
parent.source.connect(this),那么下面看看其中的源码实现:

上 述 源 码 中 作 为 参 数 传 入 的 cancelSupport , 即
FluxRefCount.RefCountMonitor , 实 现 了 Consumer<Disposable> 接
口:
也 就 是 说 , 在 第 1 次 满 足 subCount==n 时 , 会 执 行
DISCONNECT.compareAndSet(this,null,r),而此时DISCONNECT的
初始状态确实是null,所以会执行成功,然后if语句不成立,当内部
订阅者减少了一个或多个(订阅关系并没有全部解除)时,后续又有
新的订阅者参与进来。在第2次满足subCount==n时,DISCONNECT的值
已经是第一次设定的PublishSubscriber对象了,CAS动作失败,也就
是 if 语 句 成 立 , 这 直 接 造 成 的 后 果 是 调 用 PublishSubscriber 的
dispose方法,也就是会进入中断结束流程:
可以看到,在调用dispose方法后,可以反映到日志中的动作就是
disconnectAction方法,在有订阅关系的订阅者数组长度大于0的情况
下,清空下发元素队列里的元素,然后下发一个错误事件。
这也是最后下发3次异常事件与日志打印输出的原因。在这里,
Reactor开发者的意图很费解,后续订阅者的加入本来就是正常的业
务,假如确实需要一个条件来整体结束并中断所有订阅关系,也不应
该如此草率,这样的用户体验很不好,也让人摸不着头脑,确实应该
在 reactor.core.publisher.FluxRefCount.RefCountMonitor # accept
中做更多的策略来避免这个问题的发生。
我 们 在 平 时 开 发 中 应 该 会 更 倾 向 于 使 用 refCount ( int ,
Duration),下面不再对它的源码进行分析,因为实现很简单,仅通
过一个Demo来说明其关键功能:
执行结果如下:
在这里,我们将advancedConnectablepro2测试用例中<1>处的注
释打开,给<2>处加上注释,这样就看不到Four的输出了。大家如果对
其中的原理比较好奇,可自行探索源码实现。

4.10 小结
至此,关于Reactor Operator的解读就暂告一段落了,本章涉及
了很多操作的细节原理,同时也告诉大家读书、读代码时不要尽信,
要有自己的思考和保持怀疑精神,通过相应的Demo来对代码进行判
断、反问。对于这些操作的使用方法,大家也可以查看官方文档最后
的操作场景使用总结,在实际开发过程中应根据使用场景对操作API方
法进行恰当的选择。若读者希望学习更多Reactor操作,可以查看本书
配套视频中的相关源码解读。
第5章 对Processor的探索
Processor是一个很特别的存在,其既是一个Publisher,同时也
是一个Subscriber,也就是说Processor可以同时行使两者的权力。
在正常的情况下,应该尽量避免使用Processor,因为其确实不好
用。我之前在《Java编程方法论:响应式RxJava与代码设计实战》一
书的操作相关章节中举过一个关于mul_Subject的例子,其现实中的应
用可以作为一个状态机来进行设计,更加强调各个Processor的衔接组
合。能力越强,责任也就越大,做出来的东西在逻辑上就可能越复
杂,不容易驾驭,这是我们必须注意的。下面简单介绍一下关于
Processor的一些内容。
通 过 Processor 接 口 可 知 , 它 有 两 个 实 现 类 MonoProcessor 和
FluxProcessor , 后 者 相 对 复 杂 , 是 抽 象 类 , 其 又 可 以 衍 生 出
UnicastProcessor 、 EmitterProcessor 、 ReplayProcessor 、
WorkQueueProcessor 和 TopicProcessor 等 几 个 针 对 场 景 的 特 定 功 能
类。
在 正 常 的 情 况 下 , 可 以 通 过 特 定 类 的 create 方 法 来 创 建 一 个
Processor实例,而若想将其单独作为一个初始生产源,则可以调用已
经 创 建 好 的 Processor 实 例 的 sink 方 法 , 通 过 它 创 建 一 个
SerializedSink 或 SerializeOnRequestSink ( 其 内 部 调 用 的 都 是
FluxCreate.createSink中的相关方法,具体为什么这么调用呢?我们
在第2章的2.7节中介绍过如何在多线程下对生产出来的元素排序,并
进行了详细的分析),比如:

假如在多个线程中调用sink.next(n)方法呢?是不是会同时产
生很多元素,而作为订阅Processor的订阅者没办法控制元素的生产顺
序,这就是说想要控制元素的生产顺序,只能从Processor这个源头做
起。
Reactor中提供了几种常用的Processor,下面来一一介绍。
5.1 UnicastProcessor详解
下面通过图5-1来展示UnicastProcessor的行为细节。

图5-1
UnicastProcessor通过一个自定义的queue来实现背压,而且需要
注意的是,其只允许有且仅有一个subscriber。UnicastProcessor可
以在多个线程中生产元素,相当于有多个生产源同时生产、下发元
素,这时如何做到元素的有序下发呢?这就要利用本章开头部分讲到
的SerializedSink来进行控制了。由下面的FluxProcessor#sink源码
可知,在默认的情况下,serializeAlways返回的都是true,这样就得
到了FluxCreate.SerializedSink<>(s),UnicastProcessor并没有
对serializeAlways进行重写。
UnicastProcessor可以作为订阅者与上游源生产者之间产生订阅
关系,如果其与多个分布在不同线程中的上游源生产者产生了订阅关
系,那么就会出现一个问题,这就是当有一个生产者的元素下发完毕
的时候,是其他线程中的生产者继续下发元素,还是会出现其他情
况?带着这个问题来看看下面的源码:
上面源码中涉及了几个关键点,下面一一进行讲解。
◎ 通过<1>可以知道,这里是通过自定义队列来控制背压程度
的。
◎ 通 过 <2> 的 subscribe 可 知 , 这 里 是 通 过 原 子 类 操 作
once==0&&ONCE.compareAndSet(this,0,1)来控制订阅者
数的,当产生第2个订阅关系的时候,果断进入else语句块,
也 就 是 下 发 一 个 错 误 事 件 , 日 志 会 输 出 UnicastProcessor
allows only a single Subscriber错误。
◎ 通 过 <3> 的 onNext 可 知 , 假 如 元 素 下 发 结 束 , 且 调 用 了
sink.next , 那 么 就 会 执 行 Operators.onNextDropped ( t ,
currentContext())。而在其他时候,会先将下发元素放
到自定义的队列中,默认实现中是一个无界队列,最后执行
drain方法。
◎ 在订阅了上游源的情况下,会在上游元素下发完毕的时候调用
UnicastProcessor对象的onComplete方法,其中的done会被
设定为true。结合<3>处的onNext,如果UnicastProcessor作
为订阅者与上游不同线程中的多个元素生产者之间存在订阅
关系,那么只要有一个生产者生产的元素下发完毕,其他生
产者生产的元素就会被直接抛弃。
接下来看一个Demo:

执行结果如下:

执行结果还是很符合前面的描述的。
5.2 DirectProcessor详解
下面通过图5-2来展示DirectProcessor的行为细节。

图5-2
从源码中关于DirectProcessor类的定义可以知道,它可以接收0
n
到 个Subscriber。DirectProcessor并不支持背压,其内没有用于存
储元素的数据结构(在DirectProcessor的onNext方法中也没有提供元
素的缓存操作)。当DirectProcessor对象作为源生产者存在的时候,
它对应的订阅者中若有一个元素请求数量为0,那么就移除该订阅者,
同 时 向 该 订 阅 者 发 送 一 个 错 误 事 件 , 其 错 误 日 志 输 出 的 是 Can't
deliver value due to lack of requests,相关源码如下:

当DirectProcessor实例作为订阅者订阅一个发布源的时候,发布
源下发元素完毕时会调用DirectProcessor实例的onComplete方法(若
中间出错,则调用其onError方法)。观察下面代码<2>处的实现,在
其内部会调用SUBSCRIBERS.getAndSet(this,TERMINATED),获取到
订阅DirectProcessor实例的订阅者,并对它们进行遍历,一一调用它
们各自的onComplete方法。
此时若DirectProcessor实例同时作为订阅者接收上游源下发的元
素 , 那 么 该 如 何 处 理 ? 此 时 需 要 注 意 ,
SUBSCRIBERS.getAndSet(this,TERMINATED)在get操作之后会紧接
着执行一个set操作,将subscribers的值设定为TERMINATED。因此在
上游源有元素下发时,会判断subscribers==TERMINATED,而后续接收
的元素都会被抛弃。
根据<3>处,当DirectProcessor作为发布源角色,有订阅者订阅
时,会先将订阅者包装为DirectInner,然后调用<4>处的add(p)方
法 , 在 <5> 处 可 以 查 看 该 方 法 的 具 体 实 现 , 从 其 中 可 以 发 现
subscribers==TERMINATED 起 到 了 穿 针 引 线 的 作 用 。 在 调 用
DirectProcessor实例的onComplete方法后,如果有新的订阅者订阅
DirectProcessor,add(p)方法会返回false。
关于起穿针引线作用的操作,前面已经给大家展示过好几次了,
其实就是在确定一个功能后,从开始到结束都要有所展现和控制,不
要虎头蛇尾。这个过程可能是在一个类中体现的,也可能是在多个类
中体现的,但终归要用一个控制点来串成一条线,这才是我要表达的
核心主题。
接下来,通过一个Demo来展示DirectProcessor类的功能细节:

执行结果如下:
在上面的Demo中,首先下发000,从源码中可以知道,这里的
subscribers=EMPTY,所以并没有看到元素的下发输出。在<2>处发生
了订阅,然后会下发xxx。在<3>处,DirectProcessor实例以订阅者角
色参与了订阅,这里就是关键所在,上游源在元素发布结束的时候会
调用DirectProcessor(作为订阅者)的onComplete方法。接下来就是
subscribers==TERMINATED这个关键点了,这也对应了后面输出的3条
onNextDropped日志。

5.3 EmitterProcessor详解
EmitterProcessor可以同时有多个订阅者,并为订阅者提供背压
支持。其也可以作为订阅者来订阅上游源,并同步向下发送元素给自
己的订阅者们。
最初,在EmitterProcessor没有订阅者的时候,它依然可以接收
上游源下发的元素,并将这些元素缓存在自定义的队列中,然后进入
drain方法,若在上游这些元素下发完毕前都没有任何下游订阅者订阅
EmitterProcessor , 那 么 自 然 就 会 调 用 其 EmitterProcessor #
onComplete方法了。因为在drain方法中会对subscribers数组遍历下
发元素,所以并不会对后加入的订阅者重复下发之前已经下发过的元
素。
在这里,还有一个比较有意思的设定,这就是EmitterProcessor
的布尔类型的本地变量autoCancel,其可以在调用EmitterProcessor
#create时进行设定,它的主要作用是在EmitterProcessor的订阅者
一个个都解除订阅且订阅关系数归0后,自动清除内部的缓存队列,并
停止接收新的订阅关系。
接着,EmitterInner继承了FluxPublish.PubSubInner,其作用是
对 类 似 Publish 操 作 情 况 下 的 多 个 订 阅 者 进 行 管 理 。 之 前 在
ConnectableFlux中已经进行了详细的讲解,此处不再赘述,大家对照
源码或者回顾一下之前的讲解内容即可,这样也能加深理解抽象方法
的设定和具体场景的实现。
然 后 , 查 看 EmitterProcessor.EmitterInner #
removeAndDrainParent 中 的 parent.remove ( this ) 调 用 , 它 是
autoCancel实现其价值的关键所在:
由 上 面 <1> 处 的 代 码 可 以 知 道 , 若 在 调 用 EmitterProcessor #
create 时 设 定 autoCancel 为 true , 而 EmitterProcessor 的 订 阅 者 为
EMPTY,就是上面所描述的情况了。
最后,查看描述EmitterProcessor的图,如图5-3所示。
通过图5-3可以看到,将第1排横箭头中的元素和下面方框中的
EmitterProcessor.create作为一个订阅关系进行考虑,然后将方框和
下面两个横箭头看作另一层订阅关系来处理,这样在设计业务和代码
的时候也就简单了很多。

图5-3
下面通过一个Demo,给大家展示一个代码细节的问题:
执行结果如下(测试时默认开启调试模式):
除了上述结果日志提示的错误外,其他都符合之前对
EmitterProcessor的分析,可以知道,EmitterProcessor的sink方法
直 接 调 用 的 是 父 类 FluxProcessor # sink 的 实 现 , 而 其 中 有
onSubscribe(s)调用,如下:
onSubscribe ( s ) 调 用 了 FluxProcessor 子 类 , 这 里 是
EmitterProcessor的具体重写实现:

可以发现Operators.setOnce(S,this,s)的代码逻辑之前也接
触过,只不过没有对其可能发生的异常进行过分析。setOnce的表面语
义是只能设置一次,不能设置第二次,也就是在重复设定的时候,就
会报一个重复设定的错误,毕竟一个订阅者同时订阅两处生产源是不
合理的,提示错误也很符合代码健壮性的需求:

而 在 我 们 的 Demo 中 , 除 了 在 emitterProcessor.sink 中 调 用 了
onSubscribe , 在
Flux.just("Hello","DockerX").subscribe(emitterProcessor)
内部也进行了调用。由此,出现上述异常也就不奇怪了。虽然这里是
我故意设计的错误,但是希望大家可以清楚异常产生的具体原因,这
样可以方便大家在平时的开发中避免出现这种问题,或出现问题的时
候可以快速解决,以更好地驾驭自己手里的武器。
5.4 ReplayProcessor详解
ReplayProcessor可以对上游元素生产者或通过它自己创建的Sink
实例下发的元素进行缓存,后来的新订阅者可以重复接收这些元素。
这是如何实现的呢?下面试着分析一下。
首先介绍一下ReplayProcessor#create所涉及的一些策略。
◎ 分为缓存无限元素和有限元素两种情况,这里缓存的是可重复
下发的元素,元素不会随着一次下发而消失,缓存的形式不
是队列,而是使用了另一种数据结构。
◎ ReplayProcessor中有一个比较常用的API,即cacheLast,其
只缓存最后一个元素,最初调用该API的时候,其实没有元素
下 发 , 只 是 返 回 一 个 create ( 1 ) , 所 调 用 的
create(historySize,false)方法的API表示所缓存的元素
个数为1,不支持无限缓存。
◎ 还有就是结合历史元素的缓存数量和元素缓存时间限制来设计
的API,即createSizeAndTimeout。
◎ 最后是通过FluxReplay.SizeAndTimeBoundReplayBuffer实现
的基于时间调度的ReplayProcessor,缓存的历史元素有过期
时间限制。
接下来对ReplayProcessor#create的一些源码进行分析。在默认
的 情 况 下 , ReplayProcessor # create 创 建 的 是 buffer 大 小 为
Queues.SMALL_BUFFER_SIZE的无界队列形式的缓存:
接下来观察ReplayProcessor#subscribe:
在这里,可以注意到,<1>处的SUBSCRIBERS的初始值被设定为
FluxReplay.ReplaySubscriber # EMPTY : EMPTY=new
ReplaySubscription[0]。然后观察<2>处的subscribe,其会首先包装
一个ReplaySubscription,然后执行actual.onSubscribe(rs)。
接 着 通 过 <3> 处 的 add ( rs ) 将 这 个 ReplaySubscription 添 加 进
subscribers 数 组 中 ( 从 上 面 的 add 方 法 可 以 看 到 , 其 底 层 通 过
System.arraycopy 进 行 了 替 换 , 统 一 通 过 CAS 操 作 来 维 护 这 个
subscribers变量,这么做的目的主要是为了不影响可能在其他线程中
执行的元素下发动作,当前线程无法对其他线程正在使用的
subscribers数组执行写操作,这样也就实现了一个版本化控制),下
面就进入buffer.replay(rs)操作了。
之前我们接触过类似于ReplayInner的包装操作,从publish操作
就开始接触了,但是并没有讲到其中设计的起始点。在这里,对其中
的知识点进行补充。
这 一 切 都 起 源 于 reactor.core.publisher.LambdaSubscriber #
dispose的代码设计。下面观察其源码:

此处的原子类操作是getAndSet,也就是先获取原来的值,再设定
新的值,最后返回并操作原来的值。可以发现,这个原子类操作针对
的是Subscription对象,而Subscription对象衔接了整个操作的执行
链,于是就可以通过此执行链来层层解除链路关系。我们自定义包装
的LambdaSubscriber,在主动解除订阅关系的时候,会作为一个终结
者而存在。
LambdaSubscriber与上游源联系的“中间人”就是Subscription
对象,也就是若想要一路平稳地解除这个订阅关系,就必须进行反本
溯源。这也是RxJava和Reactor库设计的精妙之处,可以做到自如收
尾。于是,在订阅者解除订阅关系的时候,在上游管理多个订阅者的
情况下,解除订阅关系的操作可以直接在一个Subscription中执行。
这也是InnerProducer接口存在的意义,该接口可以根据自身的特点对
Subscription 接 口 中 的 cancel 进 行 特 定 的 实 现 。 这 也 是 将
Subscription接口中的cancel作为核心方法来设计的原因之一。
接下来,回到ReplayProcessor,来看看其onNext方法的源码:

在buffer对象的isDone返回false的情况下,将下发元素添加到该
buffer中,然后遍历之前对订阅者进行包装的ReplaySubscription,
一 一 下 发 元 素 。 在 默 认 的 情 况 下 , buffer 指 的 是 buffer=new
FluxReplay.UnboundedReplayBuffer<>(historySize)无界缓存。此
处也是重点所在,毕竟ReplayProcessor整个核心业务就是在这里设
计、展开的。下面来看看其中的具体实现。
首先来看看UnboundedReplayBuffer本地字段的定义和构造函数:
上 述 源 码 类 似 于 链 表 的 设 计 ( 可 以 对 比 RxJava 2 中 的
io.reactivex.internal.util.LinkedArrayList ) , 初 始 的 时 候 head
和tail都为n,而batchSize代表这个链表上每一个节点的容量,也就
是一个包含了batchSize个元素及指向下一个数组的引用的数组。
接着,看看UnboundedReplayBuffer的add方法:
在往buffer中添加元素的时候,会添加到tail数组中,获取当前
的tailIndex,其若在数组的最后一个位置(a.length-1),则新建下
一个数组b,然后将b的引用放在上一个数组a的tailIndex的位置,即
a[i]=b,将下发元素存于b[0]位置,即b[0]=value,同时将指向下一
个位置的引用设置为1,即tailIndex=1。若tailIndex指向的是其他位
置,那么就将值设定在这个位置上,并将此位置+1赋值给tailIndex,
即tailIndex=i+1,最后的size++表示已下发元素的个数。
UnboundedReplayBuffer中我们关心的另一个方法replay如下:

我们一般会用replayNormal方法实现正常的调用,因为这个方法
的实现细节比较多,下面将一小段、一小段地对其进行解读。
首先,Processor是一个中间者,在上游下发元素时,它会充当一
个订阅者的角色,此时会调用它的onNext方法进行元素的下发。同
时,Processor作为源生产者的作用也得发挥出来,通过onNext方法将
上游源下发的元素再下发给订阅自己的二级订阅者。这个关系需要梳
理清楚,其虽然简单,但非常重要。下面再来看看ReplayProcessor的
onNext方法的源码:

上面解读了b.add(t)的内部实现,在上面的源码中,b.replay
传入的参数类型为ReplaySubscription,即ReplayInner实例。针对每
一个ReplayInner实例,因为订阅者加入的先后顺序是不一样的,所以
在获取元素时,理应给自己打一个标记戳index,标记元素获取到哪个
位置了,此标记针对的是ReplayInner实例消费到上游源所有下发元素
中的某个位置,如果该消费index等于UnboundedReplayBuffer中定义
的size,那么ReplayInner实例已经将UnboundedReplayBuffer所缓存
的元素消费完毕。
另外,因为获取的是UnboundedReplayBuffer中定义的每个数组中
的元素,所以需要数组位置索引才能确定所获取的元素,此处将其设
定 为 tailIndex 。 同 样 , 当 前 获 取 的 元 素 所 在 的 数 组 应 该 存 储 于
ReplayInner中,这样才会方便获取下一个元素,也符合使用习惯。这
样也就有了针对ReplayInner字段的设计:
明白上面字段的设计意义后,接下来的事情就好办了。首先获取
ReplayInner对象要获取的下发元素的相关信息:
然后,对照着下面一段源码来看接下来的流程。在rs.requested
数量与循环标记元素e不相等的情况下,判断rs是否已经Cancelled,
如果返回false,则跳过此语句块。
接着判断所获取元素的总index是否已经达到size,若达到且done
为true,就设定rs的node为null,然后调用rs内部包裹的下游订阅者
(也就是前面所提到的二级订阅者)的onComplete方法。另外,done
为true的情况分两种,即正常结束和出现异常,而若done为false,跳
出while循环即可。
继续判断tailIndex是否与batchSize相等,即若tailIndex==n为
true,就通过node[tailIndex]获取指向一个数组的索引对象。当跳出
while(e!=r){...}循环时,会将最后保存的那个数组的临时变量赋
值给ReplayInner对象,即在for循环最后执行rs.node(node)。
此处还要做一件事,那就是在tailIndex==n(即ReplayInner对象
消 费 到 一 个 数 组 中 的 最 后 一 个 位 置 ) 时 , 通 过 node= ( Object[] )
node[tailIndex]来获取ReplayBuffer中的下一个数组,以便继续消费
后面的元素,同时,使tailIndex指向所获取新数组的第一个位置,即
tailIndex=0。接着从数组中获取相应的元素,然后进行元素的下发即
可 。 ReplayInner 对 象 所 包 裹 的 这 个 二 级 订 阅 者 获 取 的 总 的 元 素 为
index+1,并将tailIndex指向下一个将要获取的元素的位置。
接着做在e的数量等于元素请求数量的时候要做的事情,前面有过
介 绍 , 这 里 不 再 赘 述 。 每 执 行 for 循 环 一 次 , 都 需 要 设 定 一 次
ReplayInner对象的字段属性,以方便在跳出循环的情况下,获取并下
发元素:
至此,大家应该对ReplayProcessor有了比较深入的理解,下面通
过一个示意图来让大家产生更直观的认识,如图5-4所示。

图5-4
接着通过一个Demo来展示ReplayProcessor中的细节:
执行结果如下:

相信大家在了解了ReplayProcessor的内部实现原理后,能够轻松
理解上面的Hello,DockerX输出。
关于sink.next("000")为什么没有产生元素下发日志,这依然
要从onSubscribe方法说起:

下面来看看Operators.validate(subscription,s):

从这里可以看到,在上游源与ReplayProcessor已经存在订阅关系
的情况下,后续若又有新的上游源要与ReplayProcessor产生订阅关系
( 即 ReplayProcessor 对 象 以 订 阅 者 身 份 订 阅 上 游 源 ) , 则
ReplayProcessor 会 拒 绝 。 若 继 续 调 用 sink.next ( "000" ) , 由
FluxProcessor # sink 相 关 源 码 可 知 , 我 们 得 到 的 sink 对 象 类 型 为
FluxCreate.IgnoreSink,在调用它的next方法时,因之前sink已经调
用了它自己的cancel方法,所以会忽略该下发元素:
另外,在replayProcessor_test这个Demo中,执行<1>处代码后,

Flux.just("Hello","DockerX").subscribe(replayProcessor)
结束时会调用ReplayProcessor#onComplete:
所以SUBSCRIBERS的值会被设定为TERMINATED,但要注意,buffer
并没有被清空,再结合下面所示的源码,当ReplayProcessor再次产生
订阅时,其虽不会加入订阅者管理行列(如下面源码中的<2>处所
示),但依然会通过下面<1>处所示的代码来消费buffer中已存在的元
素。
若是将上面replayProcessor_test这个Demo中<1>处的代码注释
掉,而打开<2>处的代码注释,重新执行代码后,会发现这个Demo中
<2>处的代码好像并未执行一样,没有任何输出。结合前面的内容,当
Flux.just与这个replayProcessor对象产生订阅时,replayProcessor
在它的onSubscribe方法中会调用上游Subscription的cancel方法,而
不会调用上游Subscription的request方法,所以Flux.just也就不会
下发元素。也就是说,在没有需求的情况下,并不会主动进行元素的
下发推送,所以看不到任何输出。

5.5 小结
至此,已经完成了对Processor中设计相对简单的几种类型的讲
解,读者可以根据需要在实际的项目中选择相应的类型来应用。接下
来,将会解读TopicProcessor与WorkQueueProcessor,鉴于这两者的
实现都比较复杂,所以专门分两章进行详解。建议读者仔细阅读源
码,如果觉得理解起来有困难,可先学习相关用法,之后再慢慢研读
源码。
第6章 TopicProcessor及Reactor中匹配
Disruptor的实现代码
需 要 说 明 一 下 , 第 6 章 和 第 7 章 会 讲 解 的 TopicProcessor 与
WorkQueueProcessor 已 经 在 reactor-core 3.3.0+ 版 本 中 被 迁 移 到 了
io.projectreactor.addons : reactor-extra : 3.3.0+ 库 中 , 并 在
reactor-core 3.4.0之后的版本中被移除,主要原因是这两者的实现
都极其复杂,但是其背后的设计思想非常好,所以我还是希望把其中
的实现细节展现给大家,帮助大家从中学到一些代码设计经验。
TopicProcessor算是一个相对复杂的Processor,Reactor借鉴了
Disruptor并发框架库,并基于自身实际情况,设计了一套匹配实现代
码。下面将通过逐层递进的方式对这套实现代码进行探索。
TopicProcessor是一个异步处理器,当基于其share方法获得一个
TopicProcessor实例时,这个实例可以转发从上游多个元素生产者下
发的元素(但同时只能订阅一个Publisher,即同一个上游源生产者在
多个线程中生产元素或者此Publisher内部并发生产元素并下发)。
需要注意,当你打算并发地调用TopicProcessor实例的onNext、
onComplete或者onError方法时,或者在TopicProcessor实例订阅了一
个并发产生元素的上游Publisher的情况下,必须进行共享设置(进行
share 方 法 的 设 定 ) , 否 则 , 这 种 并 发 调 用 就 会 报 错 。 另 外 ,
TopicProcessor本意是基于一个主题进行元素的下发消费,订阅者往
往要求元素按生产顺序消费(即便是多线程消费,也要按下发元素的
顺序消费)。
在这里,每一个TopicProcessor实例的订阅者都会被分配到一个
唯一的线程中,当Auto-Cancel可用时,即在autoCancel为true的情况
下,只有TopicProcessor实例的所有订阅者都解除了订阅关系,才会
发送一个cancel信号给上游源生产者,之前已经分析过这个cancel
了,即TopicProcessor实例作为订阅者会调用这个cancel方法来解除
与上游源生产者的订阅关系。
Executor是可以自定义的,在它(并不是requestTaskExecutor,
需要读者注意)确定固定线程的数量之后,也就确定了到底允许多少
个Subscriber可以并发执行。当一个Subscriber的元素请求数量为
Long.MAX的时候,不支持背压,也就是说,如果订阅者的消费能力匹
配不上元素的生产能力,上游源生产者将会产生风险(比如之前讲过
的Flux#interval方法实现)。
当Publisher(这里指TopicProcessor实例)消耗完下游的元素请
求 数 量 时 , 它 的 订 阅 者 将 无 法 再 读 到 Next 信 号 , 但 还 可 以 读 到
Complete和Error信号。
当不只有一个Subscriber订阅TopicProcessor实例的时候,在这
些Subscriber的元素请求数量没有被耗尽的情况下,它们会接收到一
样的事件(即每个订阅者都可以获取到这次下发的元素),这种情况
类似于扇出[1] 。
TopicProcessor用一个RingBuffer数据结构(即环形队列)来存
储上游推送来的信号元素,每个Subscriber线程都会跟踪与之相关的
request需求及其在RingBuffer中的正确索引。

6.1 初识TopicProcessor
本节将带着大家梳理一下TopicProcessor的设计思路,使大家对
它有一个比较清晰的认识。
从TopicProcessor的字面意思来看,其会根据一个主题来进行元
素的消费,所以上游应该只有一个Publisher可以在多个线程内执行元
素的下发操作。TopicProcessor可能有多个订阅者,这些下游订阅者
会接收订阅时下发的元素,但每一个订阅者所做的事情可能不同,消
费一个元素的时间也可能不同,那么就需要进行上下游之间元素的存
储与消费进度控制,同时由前面的内容可知,当前所有订阅者都消费
过的元素没有保存价值,因此这里使用环形队列来做一个衔接上下文
的基础数据结构,通过游标和相对位置并结合CAS操作来进行协调控
制。
这里遵循的设计思路是:通过审题,可以总结出几个关键点,围
绕关键点进行设计,在根据关键点做功能结合设计的时候,你会推导
出相应的数据结构,整个过程会是一个很自然的过程。
对照上面这个设计思路,结合TopicProcessor的内部源码,下面
通过两个图来展示大致的过程,首先是上游源生产者与环形队列的交
互,获取可存储元素的位置,如图6-1所示。

图6-1
与之相应的是环形队列与多个订阅者之间的交互,如图6-2所示。
图6-2
图6-1和图6-2表现了TopicProcessor的内在设计原理,但其终究
要 符 合 响 应 式 规 范 , 所 以 结 合 我 们 自 己 的 分 析 , 如 果 上 游 Topic-
Publisher在一个单线程内生产下发元素的话,结合响应式规范,它的
整体状况应该如图6-3所示。
图6-3
接下来我们来一点一点地对TopicProcessor进行分析。在这里,
先来看TopicProcessor#create,因为这个类的配置项比较多,所以
使用了Builder,如下:
由上面的源码可知,需要配置一些我们需要的工具,其中有两个
ExecutorService(即executor与requestTaskExecutor),一个是用
来分配订阅者线程的(面向下游消费者),另一个是用来执行请求任
务的(面向上游源生产者)。
在这里,因为RingBuffer需要根据自身长度与所传入的数值做&运
算来快速确定偏移量,所以此处的bufferSize必须为2的 次方。在 n
create方法中,由于share的默认值为false,所以通过create方法得
到的TopicProcessor对象不适合上游并发产生元素的场景,因此在图
6-3中可以看到方框中有一句0-1 thread if parent onSubscribe,意
指如果有上游源的话,其只能在一个线程内产生元素。在本章的最
后,我会通过一个例子给大家展示share配置下单生产者在多线程中生
产下发元素的情况。
TopicProcessor会根据share的不同进行不同的配置,在其构造函
数内针对RingBuffer也会创建相应的匹配对象,本章也会对其中涉及
的MultiProducerRingBuffer复杂类进行解读。
接着来看share方法的执行过程示意图,如图6-4所示。
图6-4
对 比 图 6-3 和 图 6-4 , 方 框 中 的 内 容 有 区 别 。 同 时 , 图 6-4 中
TopicProcessor的第2个订阅者在订阅期间发生了一个错误,即第2个
订阅者解除了订阅关系,但不影响第1个订阅者请求元素的下发、获
取。而且图6-4中方框上方的3个圆与下发箭头所代表的颜色不同,这
表示上游源产生的3个元素在不同的线程中进行下发。接下来,一起来
看看其内部到底做了什么,各数据结构之间会产生怎样的化学反应。

6.2 TopicProcessor构造器
为降低难度,我们会先介绍各个小组件,大家在学习的过程中注
意结合6.1节的3张示意图,下面看看share的源码:
与create比较,这里就是将share设定为true,其他部分一样,下
面看看TopicProcessor的构造器:
上面的源码中有几个陌生的东西,下面先来看Slot:

可以看到,Slot的定义很简单,其就是一个可重用的数据容器,
用 于 定 义 环 形 队 列 RingBuffer 的 槽 。 接 着 来 看
RingBuffer.newSequence:

Sequence是用来跟踪RingBuffer和事件处理进度的并发序列类,
其 支 持 多 种 并 发 操 作 , 如 CAS 及 顺 序 写 入 。 在 这 里 , 调 用 的
newSequence如下:
默认hasUnsafe会返回true,下面直接看UnsafeSequence,其为
RingBuffer.Sequence接口的实现类,其同时还继承了RhsPadding类:
可以看到,其操作数据的核心依然是原子类,如果想要通过原子
类来对字段进行相关操作(即UNSAFE.objectFieldOffset(...)),
需要先将该字段声明为volatile变量,这里指value。关于原子类操作
的细节,之前已经分析过,这里不再赘述。
完成上面的讲解,下面再说一说一些基于RingBuffer的调控调度
策略。这些调度策略会涉及对RingBuffer读/写操作的调度。
首 先 通 过 下 面 这 段 源 码 来 对 RingBuffer 读 操 作 进 行 解 读 。
TopicProcessor构造器中的RingBuffer.Reader barrier是一个很重要
的组件,其可以通过给定的等待策略(waitStrategy),使用游标序
列 和 消 费 序 列 来 调 控 RingBuffer 元 素 的 生 产 者 和 消 费 者
(consumer)。
表面上,Reader给我们最多的就是waitFor、signal、alert等类
似于AQS、CyclicBarrier、CountDownLatch的相关底层实现,而这些
实现的核心就是对线程的控制与分配。其中的核心方法waitFor内涉及
了 两 个 代 码 逻 辑 , 一 个 是 waitStrategy.waitFor , 另 一 个 是
sequenceProducer.getHighestPublishedSequence , 前 者 从 订 阅 者 的
角度考虑消费进度,后者是生产者可提供的最大消费进度。对于后
者,后续还会详解,这里先来讲解waitStrategy.waitFor的实现细
节:
其 实 就 是 给 定 一 个 序 号 位 置 sequence , 对 于 传 入 的
cursorSequence,其类型就是Sequence。之前有介绍过Sequence的源
码,作为LongSupplier角色,Sequence调用getAsLong方法返回了它的
子类内部定义的volatile变量字段value的值。我们再结合注释,需要
等待给定的序列号可用(即生产者将新元素存放在其中),如果
cursor得到的值小于sequence,也就是还没等到新元素到来,此时可
行使的职责就是继续等待或者离开(这里可能会产生运行时异常,这
是因为Runnable的run方法在它的执行线程运行的过程中可能出现打断
异常InterruptedException),这个方法默认用于超时类型的实现,
后面会对此进行具体的分析。
根据不同的使用场景,WaitStrategy有多种不同的实现。此处,
我 们 在 reactor.core.publisher.TopicProcessor.Builder # build 方
法 中 可 以 看 到 this.waitStrategy=this.waitStrategy ! =null ?
this.waitStrategy:WaitStrategy.phasedOffLiteLock(200,100,
TimeUnit.MILLISECONDS ) ; , 在 默 认 的 情 况 下 , 并 没 有 对
this.waitStrategy进行设置,也就是说,这里的等待策略默认使用了
WaitStrategy.phasedOffLiteLock,源码如下所示:
首先,phasedOff策略是针对消费者的等待策略,这里的barrier
可以看作轮号游戏,也就是作为消费者想要自己的需求得到满足,打
个比方,若我们将消费者看作破壳而出的雏鸟,鸟妈妈得给雏鸟喂
食,先破壳的雏鸟就先获得虫子,而后破壳的雏鸟会和之前的雏鸟一
起获得虫子(这里的虫子可以看作下发的元素,一次可以下发很多
“虫子”,不只是一条“虫子”),而这一切都是有序的,如破壳的
顺序、生产虫子的顺序,以及后破壳的雏鸟只能吃到它第一次收到的
和之后的虫子序列。在没有新的虫子出现之前,这些嗷嗷待哺的雏鸟
只能等下一波虫子生产出来。barrier作为中间者只进行控制,并不掌
控真实元素。这也是Reactor参考并发框架Disruptor进行设计的核心
场景。
n
这其中的控制比较多,比如若雏鸟闭着眼睛张嘴 次没有虫子送入
口中,那么它就会进入睡眠状态(挂起)来等待成年鸟唤醒它并喂
食,另外,雏鸟夭折或者母鸟出意外的情况也要考虑,这也就是雏鸟
在run方法里要做的事情。
当我们对吞吐量和低延时的需求度比不上对CPU计算资源的需求度
的时候,可以通过Thread.yield来让出当前线程所占用的CPU计算资
源。即在业务更注重快速得到计算结果的时候,很适合使用phasedOff
策略,如有必要的话,可以配置超时控制策略。
结合上面的TopicProcessor.TopicInner#waiter相关源码,对于
waiter的running值,除非调用TopicInner的halt或TopicInner的run
方 法 运 行 异 常 后 将 该 值 设 定 为 false ( 这 里 不 对
processor.isTerminated进行说明),否则,直接跳过。
如果while循环10000次,cursor.getAsLong())>=sequence依
然不成立,那么就进行超时判断起始时间的设定。如果下一次执行
10000次while循环,cursor.getAsLong())>=sequence依然不成立
(即雏鸟依然看不到虫子),就进行超时判断:当超时timeDelta大于
100 ( yieldTimeoutNanos 在 TopicProcessor 中 的 默 认 值 ) 时 , 调 用
LiteBlocking # waitFor 方 法 , 进 入 锁 操 作 , 这 里 依 然 是 为 了 使
cursorSequence.getAsLong ( ) ) >=sequence 成 功 , 不 成 功 便 进 行
Condition锁排队,同时线程进入挂起状态。源码细节如下:
此处代码较多,但希望大家不要把它看得过于复杂,毕竟它们都
是基础知识的应用,而且我们曾反复接触过。另外,阅读源码的时候
注意结合前面的示意图进行理解。

6.3 对RingBuffer中publish方法的解读
从前面的TopicProcessor.TopicInner包装来看,其作为消费者会
调 用 RingBuffer.Reader # waitFor , 即 内 部 调 用
waitStrategy.waitFor方法,以等待(也可能不需要等待)获取生产
者生产的匹配自己的元素序列号。此处,消费者不需要等待的情况
有,因自身消费元素的速度比较慢,慢于元素的生产速度,其消费进
度甚至可能会影响上游元素的入环进度。
因为生产的元素是从上游过来的,作为中间者,TopicProcessor
将元素下发给它的订阅者调用的是onNext方法,这也是元素入环的关
键业务:

这里的代码逻辑是,先从环形队列中获取下一个可允许存储元素
的序列号,然后根据这个序列号获取相应的存储数据的Slot对象(之
前已经讲解过Slot,其用于存储一个元素,是一个很简单的数据结
构),然后将这个Slot的值设定为所下发的o,接着将这个准备好的元
素所在的序列号通过ringBuffer.publish(seqId)发布出去,以方便
等待着的订阅者获得可用序列号。
读者在这里可能会产生疑问:如何保证环形队列中存入的数据的
槽Slot与订阅者要获取的数据的槽Slot不冲突。举一个简单的场景,
假如一个环形队列总共有12个槽,消费者1读到了第9个槽,消费者2读
到了第5个槽,那么生产者在更新数据(即执行ringBuffer.next方
法)到第5个槽的时候,刚好消费者2比较懒,处理得比较慢,还没处
理完,那么就会发生冲突。所以,这里就需要进行控制。因为是生产
者下发数据,那么必定存在针对数据入环的控制操作,而数据的消费
操作只是对环内数据进行读取,所以我们可以不需要考虑消费者有多
复杂,只需要考虑在其读取数据的位置设置位置锁,而这个位置锁是
做在barrier之上的,之前有讲解过。那么这里针对生产者来设计一个
RingBuffer拓展类UnsafeRingBuffer,为RingBuffer加入一些针对当
前场景下的元素进行添加与读取的控制API,将barrier控制加入其
中。下面来看看具体的实现过程:
从EventLoopProcessor#onNext方法调用的ringBuffer.next可以
得 到 上 面 <1> 处 的 代 码 , 也 可 以 知 道 , 此 处 代 码 源 自
UnsafeRingBuffer , 这 个 RingBuffer 的 值 来 自 TopicProcessor 父 类
EventLoopProcessor 的 构 造 器 ( <2> 处 的 代 码 ) 中 的
RingBuffer.createMultiProducer 。 下 面 来 看 看
RingBuffer.createMultiProducer 中 的 几 个 参 数 , factory 为
Supplier<Slot<IN>>的一个lambda函数,从TopicProcessor的构造函
数可以看到,之前讲过这个函数。中间两个参数比较简单,不详细解
释了。最后说一下this参数,此时它(EventLoopProcessor)是一个
Runnable对象,EventLoopProcessor并没有对Runnable接口的run方法
进行实现,需要在它的子类中实现,也就是上述源码中<4>处的代码实
现,这里是一个开关语句条件判断,其实就是为了确认这个Processor
还能否使用,在能用的情况下判断TopicProcessor的订阅者数是否为
0,条件为真就抛出异常并跳出当前线程。
明确了上面的内容,下面进入<3>处的MultiProducerRingBuffer
逻辑包装类进行详细讲解。由上面的源码片段可以知道,
UnsafeRingBuffer 中 的 next 和 publish 方 法 都 是 由
MultiProducerRingBuffer 对 象 实 例 实 现 的 。
MultiProducerRingBuffer为RingBufferProducer的一个拓展子类,而
RingBufferProducer也是Disruptor并发框架库中的重要的一环。接下
来,通过MultiProducerRingBuffer来具体讲解RingBufferProducer的
内在功能实现。

6.4 对MultiProducerRingBuffer的解读
由前面的内容可以很容易地知道,RingBufferProducer就是一个
针对不同生产情况(单或多生产源的情况)来执行序列操作的基类。
而往环形队列里添加元素,其实不需要考虑这么多,而只需要考虑要
操作的位置是否可用。假如将这个要操作的位置看作游标的话,那么
RingBufferProducer就是用于维护这个游标所有权的,以及通过对该
所有权的控制来执行元素的添加或删除操作(比如需要对某个槽进行
更新,然后提交、等待、执行完毕、删除事件),这有点类似于Web开
发中针对事务的管理操作。
用一句比较通俗的话来形容,就是占位。也就是说,我们必须知
道消费者正在操作的槽位(比如有12个槽,消费者消费得比较慢,才
消费到第5个槽,这样在后面的几个槽位上就不能进行赋值操作,这几
个槽位被称为消费者占槽),这些信息都需要进行统计,这样才方便
我们根据这些信息进行管理。
而这里也放置了RingBufferProducer与消费者进行交互的接入点
RingBufferProducer#newBarrier,相信大家应该没有忘记之前在讲
解RingBuffer.Reader#waitFor方法时谈及的关于面向生产者控制的
sequenceProducer.getHighestPublishedSequence 方 法 吧 , 它 用 于 得
到可供读取的最大序列号。下面来分析一下其中的源码细节:
下面来看看MultiProducerRingBuffer的构造函数。首先看它的父
类,这里涉及的字段有spinObserver、bufferSize、waitStrategy,
且cursor就是游标,它是针对生产者来说的,指向生产者已经生产的
并放在环内的最新元素所在的位置,这个位置最初没有放任何元素,
也就是其初始值为-1。可以认为gatingSequences是那些有操作冲突的
槽对应的序列位置,比如上面说的消费者占槽。
下面来介绍一下RingBufferProducer子类,在这里可以给大家带
来点新知识,即JDK中隐藏的“扇贝”。
6.4.1 RingBuffer中的UnsafeSupport
前面介绍过Unsafe,它和并发处处相关,由于JDK的安全机制,很
难 将 Unsafe 直 接 拿 出 来 使 用 , 刚 好 RingBuffer 里 也 借 用 了 Netty 对
Unsafe的支持实现,本节索性就给大家讲解一下相关知识。
首先,从MultiProducerRingBuffer类的定义可以知道,其通过
RingBuffer.getUnsafe得到了UNSAFE实例,下面来研究一下其内在操
作原理:
在 上 面 的 源 码 中 , 首 先 通 过 反 射 操 作
Unsafe.class.getDeclaredField ( "theUnsafe" ) 得 到 了
unsafeField,接下来执行maybeUnsafe=unsafeField.get(null),
传入了一个null,这里的代码可能会使部分细心的读者产生疑惑,为
什么传入一个null?关于此处,下面来看看Field#get方法的定义:

这个方法的功能就是返回给定对象中对应字段(Field)的值。但
当字段为静态字段的时候,该字段属于类本身,在JVM加载class字节
码的时候会对静态字段进行初始化,即调用此方法的这个字段不属于
给定对象,所以即便传入一个对象(哪怕是null),也会被无视和忽
略,这就是我对java.lang.reflect.Field#get注释的解释。另外,
希望不明白的读者可以回顾一下JDK的基础内容。
下面来观察sun.misc.Unsafe中对theUnsafe字段的定义:

从上面的源码可以很轻易地看到,theUnsafe是一个静态final字
段,在设定完这个字段之后,根据final的语义可以知道无法再修改这
个字段。在这里,JDK也给出了一个可以简单地得到此theUnsafe指向
的Unsafe实例的方法getUnsafe,JDK也提供给我们使用建议(可以查
看上面源码中的注释)。应该谨慎使用Unsafe对象,该对象可以直接
通过给定的内存地址来操作数据,不要将其用于不太确定的代码。举
个例子,在对某个公共变量进行多线程操作的过程中,往往线程中会
保存一份该公共变量的数据,假如操作的这个公共变量不是volatile
变量,那么Unsafe操作就会直接通过内存地址在内存中修改数据,而
这一操作并不会引起线程局部缓存TLAB的刷新,这样就会出现危险。
这也是为什么我一直强调使用Unsafe操作的字段最好用volatile修
饰。所以,我们在很多时候是不是可以如下这样使用Unsafe对象呢?

可是,当我们执行代码的时候,假如使用的是JDK 8版本,就会报
程序包sun.misc不存在的错误,即sun.misc.Unsafe是JDK的内部类,
因此如果我们做的是Maven项目,可以对maven-compiler-plugin插件
进行如下设置(配置compilerArguments):
而 假 如 我 们 使 用 的 是 JDK 9+ 版 本 , 就 会 出 现
java.lang.SecurityException:Unsafe错误,也就是说,我们没有权
限直接使用Unsafe,那么可以仿照Netty中的用法,进行反射获取,下
面来看一个Demo:

执行结果如下:

这也是Reactor的源码内放着简单的API不用而要通过反射调用得
到Unsafe实例的原因。对Demo中一些细节的解读如下。
◎ arrayBaseOffset:返回当前数组第1个元素地址相对于数组起
始地址的偏移量(根据数组对象在内存中分配地址的规则,
因为本例中是int[],所以其值为6,读者可以查阅相关资料
具体了解,此处就不多做解释了)。
◎ arrayIndexScale:返回当前数组一个元素占用的字节数,本
例中返回的是4。
◎ putInt(Object o,long offset,int x):获取数组对象o
的起始地址,加上偏移量,得到对应元素的地址,将x写入该
地址所指内存中。
◎ getInt(Object o,long offset):获取数组对象o的起始地
址,加上偏移量,得到对应元素的地址,从而获得元素的
值。
◎ 偏 移 量 : 数 组 元 素 地 址 偏 移 量 等 于
arrayBaseOffset+arrayIndexScalse*i。
知道上面这些,估计大家对MultiProducerRingBuffer中的BASE与
SCALE也有了一定的认识,RingBuffer的很多内部类的定义都有使用到
上面这些细节,大家在阅读相关源码的时候可以注意一下。
下面重新回到MultiProducerRingBuffer的构造函数中:
可以看到,代码中创建了一个对可用元素的序列号进行缓存的数
组,也就是可用的最大元素数量就是环形队列中元素的总数量。接着
就是上面的UNSAFE操作,刚开始的时候,没有元素可用,就将对应位
置的序列号值全都设定为-1。
6.4.2 RingBuffer中的next与publish操作
接 着 回 到 EventLoopProcessor # onNext 中 , 当 代 码 执 行 到
ringBuffer.next时,来看看这里如何结合前面的知识通过RingBuffer
来对元素的下发和消费进行协调调度:
经过前面的分析,可以知道,假如有多个生产者的话,最后调用
的是MultiProducerRingBuffer#next方法:
在上面的源码中,首先获得了当前的游标值current,然后就可以
得到其下一个位置next。执行wrapPoint=next-bufferSize;后会出现
一个场景,假如有一个400m的环形跑道,小明跑了599m,对小明来
讲,他要获取的下一个值就是600,也就是next,他在环内的位置也就
是wrapPoint,在这个场景下,即距离开始位置200m的地方。与此同
时,小强如果在距离开始位置300m的地方,我们给这里加一个限制条
件,即在整个过程中不允许超人一圈,也就是跑得快的小明在即将超
过小强一圈的时候,只能跟在小强后面。结合这个场景,大家就能更
好地理解下面的内容了。
在 之 前 MultiProducerRingBuffer 的 定 义 中 有
gatingSequenceCache=new
UnsafeSequence(RingBuffer.INITIAL_CURSOR_VALUE),它用来表示
有存储生产限制的最小的参考序列号(在这里,可以认为其是最慢消
费者所在消费位置的参考序列号)。想必大家已经知道,上述源码中
的cachedGatingSequence所代表的含义了。于是,接下来就从最慢消
费者与生产者之间的进度关系开始着手。
在生产者“超过一圈”后,即wrapPoint大于0,紧接着,其值大
于最慢消费者所指向的参考序列号(因为小明想取的值在下一个位
置,即wrapPoint,也就是参考序列号+1),此为生产者等待进行额外
处理的条件之一。当这个条件不成立的时候,再来判定当前最慢消费
者所指向的参考序列号大于当前游标所指向的序列号这个条件是否成
立。
只要上述这两个条件中的任何一个成立,我们就都需要进行相应
的关系处理,要知道,我们面对的是多个订阅者的情况,因为消费者
的消费速度不一致,而且有可能会慢于元素生产、下发的速度,所以
就 有 可 能 会 存 储 多 个 位 置 序 列 限 制 , 于 是
RingBuffer.getMinimumSequence(gatingSequences,current)也就
代表着从当前游标和众多限制参考序列号中找一个最小值。
而当上游源生产者下发元素到环形队列中时,需要先获取下一个
要存储元素的序列位置,如果已达到最慢订阅者正在消费位置的参考
序列号,那么在获取下一个位置时就应该等待,直到该订阅者消费完
手中的元素,获得下一个位置的元素为止。如果该最慢订阅者的位置
用 cachedGatingSequence 表 示 , 那 么 wrapPoint 指 向 的 其 实 是
cachedGatingSequence+1。所以如果上游源生产元素很快,那么在即
将 超 越 最 慢 订 阅 者 一 圈 的 时 候 , 此 时 next-bufferSize 与
cachedGatingSequence+1 是 相 等 的 , 即
wrapPoint>cachedGatingSequence , 那 么 就 要 等 待 。 需 要 记 住 ,
wrapPoint主要针对的是“超过一圈”的情况。
如果订阅者消费元素的速度比生产者生产、下发元素的速度快,
那么订阅者的序列号和环形队列的游标值相等。当没有生产足够填充
环形队列一圈的元素的时候,cachedGatingSequence是小于或等于
current的,这时只需要通过CAS将cursor值设置为下一个位置即可
(对于小于或等于的定义是,若元素消费得比较慢,那么就是小于,
而若元素消费速度跟得上元素生产速度,那么就是等于)。
那么为什么会有cachedGatingSequence > current这个条件存在
呢?在初始化时,游标代表的值和gatingSequenceCache的值相等(初
始时都为-1),按照当下的代码逻辑,gatingSequenceCache值是不可
能超过游标值的,这里为什么要对它进行判断呢?当上游源生产者在
多 个 线 程 中 生 产 、 下 发 元 素 时 , 就 要 对 cachedGatingSequence 和
cursor进行考虑,它们都是内存级别的操作(都基于Unsafe,如下面
的源码所示),属于共享变量,可能在一个线程中刚获取了current,
在 另 一 个 线 程 中 就 修 改 了 cachedGatingSequence , 这 样
cachedGatingSequence>current就有可能成立,所以要针对这一情况
进行判断。
下面再次回顾、强调一下,最慢消费者正在消费位置的参考序列
号就代表着新元素能存储到环形队列中的那个最大的序列号所在的槽
位。所以说,当current等于cachedGatingSequence时,游标已经放在
了最慢消费者所参考的游标位置上,这个最慢消费者已经读完或者等
待读这个游标的下一个元素(可能最慢消费者和最快消费者是等同
的,即同一个角色),生产者就可以覆盖这个游标指向的下一个元
素。最慢消费者已经将这个元素读取完毕,只是还在消费中,或者正
等着最新的元素。
然 后 , 根 据 EventLoopProcessor # onNext 中 与 final
Slot<IN>signal=ringBuffer.get(seqId)对应的可赋值序列号,得
到对应数组中的slot对象,并将其value字段值设定为我们所生产的元
素signal.value=o。
最 后 , 执 行 到 EventLoopProcessor # onNext 的
ringBuffer.publish(seqId),终于到了与订阅者接轨的时候了,结
合前面对消费者waitfor逻辑的讲解,publish的实现就是首先设定可
用序列号,然后唤醒订阅者等待线程。具体源码如下:

在这里,相关源码已经在前面讲解过了,不再赘述。关于单生产
者的细节也不再解读,感兴趣的读者可以通过源码与多生产者的细节
进行对比。
至此,关于Reactor 3对Disruptor并发框架库的第三方实现已经
讲解完毕(限于篇幅,读者还可以通过源码自行深入更多细节),这
算是一个很漫长的过程,希望读者细细品味,从中获得一些源码设计
上的启发。

6.5 TopicProcessor.onSubscribe及类
BossEventLoopGroup的设计
本节将介绍TopicProcessor作为订阅者与上游源生产者之间的一
些事情。当产生订阅的时候,对于订阅者来说,首先会调用它的
TopicProcessor.onSubscribe方法,也就是TopicProcessor父类中定
义的onSubscribe:

在上面的源码中,主要需要关注requestTask的实现。其中首先获
取了RingBuffer的当前游标所指位置,可用其设定minimum。通过前面
章节中的内容可以知道,RingBuffer的addGatingSequence方法内部使
用 了 一 个 装 饰 模 式 , 由 RingBufferProducer 的 实 例 对 象 的
addGatingSequence方法实现。也就是说,此处会将minimum加入限制
操作序列的数组gatingSequences中。
接着,在一个线程池中执行请求任务:
这里通过requestTaskExecutor.execute将请求需求单独放在一个
线程中处理。之前在EventLoopProcessor#onNext中只介绍过入环操
作,并没有涉及元素需求。假如有多个生产元素的渠道(多生产者单
订阅者是违背设计原则的,同一时刻只能有一个生产者Publisher,即
EventLoopProcessor作为订阅者与上游源生产者其实只能存在一个真
正的订阅关系),那么就会在多个线程中调用EventLoopProcessor#
onNext方法,也就有可能同时对同一个RingBuffer进行操作,这时会
发生冲突,这点前面已经通过MultiProducerRingBuffer解读过了。
但我们还要思考一下,TopicProcessor既然有环形队列做缓存,
那么就要结合环形队列做一些类似于背压的协调机制,即下游订阅者
要通过TopicProcessor向上游发出请求,上游也要根据下游的消费进
度安排元素的生产,而这个请求上游的动作是无法放在下游消费者层
面做的,对于上游来说,TopicProcessor就是它的订阅者,于是可以
在一个单独的线程中执行TopicProcessor向上游发送请求的操作。此
处可以借鉴Netty的boss线程池设计,可以将TopicProcessor中设定的
请 求 任 务 设 计 为 一 个 EventLoop , 并 放 到 requestTaskExecutor 中 执
行,这样看起来就类似于Netty中的BossEventLoopGroup了。
在这里,EventLoopProcessor作为订阅者,需要执行以下操作。
◎ 首先,通过Subscription的request方法发送请求:此处临时
变量limit在bufferSize为1时被设定为1,在其他情况下,其
被设定为bufferSize的二分之一。接着将初始游标值设置
为-1,若要判断是否需要请求元素,首先要判断该Processor
是否不可用,如果可用,进一步判断该TopicProcessor的下
游订阅者数量是否为0。如果两个判断有一个成立,那么就抛
出一个异常信号(见上面TopicProcessor#run的源码)。接
下 来 就 是 向 上 游 发 起 请 求 , 即 执 行
upstream.request(bufferSize)。
◎ 然后,EventLoopProcessor作为订阅者要与上游保持联系,也
就是要确保自己能够动态监控元素请求数量,当达到定义的
界限值时就再次刷新元素请求数量(也就是再次发出请
求 ) 。 下 面 对 具 体 操 作 分 析 一 下 : 可 以 看 到
parent.readWait.waitFor(c,readCount,parent),其中
的 readWait 字 段 EventLoopProcessor 被 定 义 为
WaitStrategy.liteBlocking , 其 内 部 的
WaitStrategy.LiteBlocking#waitFor方法表示当游标所指
序列号小于我们传入的c时,就等待着(线程挂起,这里利用
最慢订阅者消费进度小于元素生产进度时挂起等待的规则来
对请求管理进行设计实现),而当游标所指序列号达到c时,
说明环形队列中的元素消耗过半,那么就再次请求添加元素
请求数量,一次添加limit+(cursor-c)个元素请求数量。
需 要 具 体 说 一 下 parent.readWait.waitFor ( c , readCount ,
parent)这句代码,当readCount大于或等于c时,才会跳出等待状
态,也就是说当下游最慢订阅者读元素的数量等于一次请求的元素数
量时,再次向上游发出请求即可。这里对这句代码进行一些其他说
明。
◎生产者和消费者的交集点就是readWait,对于TopicProcessor
来 说 , 这 里 的 readCount 是 一 个 函 数 表 达 式 ( ) ->
SUBSCRIBER_COUNT.get ( TopicProcessor.this ) ==0 ?
minimum.getAsLong ( ) :
ringBuffer.getMinimumGatingSequence ( minimum ) ) , 其
值与下游针对TopicProcessor的订阅者数量息息相关。在多
订阅者的情况下,会返回其中最小的那个限制序列号,当只
剩一个订阅者时,就按照只剩这一个订阅者的minimum值返
回。初始时minimum为RingBuffer当前游标所在的位置。每循
环一次,就会刷新一次readCount,即通过minimum::set重
新 设 定 minimum 值 为 当 前 获 得 的 RingBuffer 的 游 标 位 置
(minimum::set为postWaitCallback的实现)。
◎ 在下游有多个订阅者的情况下,由于每个下游订阅者的消费速
度可能不同,这导致环形队列对上游元素的获取、插入的需
求也有所差别,这时就要通过readWait来进行控制。所以说
两个看似分离的EventLoop其实还是有联系的,这个联系由一
个相对解耦的存在来实现。此时的等待状态(readWait)由
下 游 订 阅 者 的 消 费 进 度 决 定 , 这 里 RequestTask 设 定 的
EventLoop与下游订阅者的EventLoop直接挂钩、对照,关于
这一点后续还会说到。
◎ 此 处 的 parent.readWait 需 要 与
this.barrier=ringBuffer.newReader区分开。readWait获得
的等待策略是基于LiteBlocking类来实现的,主要用于协调
上游元素生产和下游元素请求之间的关系。而this.barrier
用于下游订阅者对元素的获取、等待,它默认是基于
PhasedOff类来实现的。

6.6 TopicProcessor.subscribe及类
WorkerEventLoopGroup的设计
对于下游来讲,TopicProcessor是生产者,我们需要看看下游订
阅者与它产生订阅时所调用的TopicProcessor#subscribe方法:

在上面的源码中,首先判断TopicProcessor的上游是否有元素下
发,即TopicProcessor的订阅者状态是否结束(该TopicProcessor的
订阅者状态是通过0==terminated来判断的,如果其不相等,就代表结
束了订阅者状态),那么现有的RingBuffer中的元素也就确定了,将
这些已经确定的元素数据作为一个源。后来产生的订阅关系都是彼此
独立的,不再顾及对方消费速度的快慢,每次订阅都会重新生成、下
发一份数据,在产生订阅关系的时候,从RingBuffer中获取元素。而
关于冷数据源数据的生产、下发,其内部优先使用Flux.generate,这
属 于 Flux.generate 的 一 个 实 践 应 用 方 式 , 具 体 参 考
reactor.core.publisher.EventLoopProcessor#coldSource的源码,
希望大家可以将其应用于自己的实际开发中,另外后面也会对冷热数
据源进行进一步的总结和解读。
下面再说明一下TopicProcessor结束订阅者状态时要做的事情,
结合之前所讲,应该是将状态值terminated由0变为1,但结束还要分
为正常结束和非正常结束。在正常结束的情况下,上游的
Subscription会被置空,解除引用关系,方便垃圾回收,然后向下游
发出结束信号,下游处理完结束信号后会将下游的executor关闭,最
后 解 除 上 游 被 挂 起 的 请 求 线 程 的 挂 起 状 态 。
EventLoopProcessor.RequestTask # run 中 指 向 上 游 源 生 产 者 的
Subscription并不会为null,但是可以知道,当上游元素下发完成
时,request请求内部会对这些状态进行判断,这里就点到为止。
通过TopicProcessor#doComplete向TopicProcessor的下游订阅
者发送结束信号,这里要做的就是给下游TopicProcessor的订阅者发
送一个解除线程挂起的信号,能挂起的都是等待最新下发元素的订阅
者。当生产者正常结束时,解除线程的挂起状态,然后关闭executor
即可,也就是当上游源的元素发布完毕时,就关闭线程池,当再次有
下游订阅者对TopicProcessor产生订阅的时候,获取的就是冷数据源
了 , 即 下 游 订 阅 者 会 订 阅 通 过 TopicProcessor # subscribe 中 的
coldSource方法得到的数据源:
下面通过一个Demo对冷数据源进行展示:
执行结果如下:

对于上面的调试日志,相信大家都应该知道其从何而来了,可以
查阅UnsafeSupport的相关源码了解更多。
topicProcessor 向 上 游 源 发 出 request 请 求 , 即 会 调 用
TopicProcessor # requestTask , 在 此 方 法 内 部 调 用
requestTaskExecutor.execute(task),也就是说,上游元素的下发
动作是在requestTaskExecutor所代表的线程池中执行的。该下发动作
与主线程是异步关系,topicProcessor.onNext("000")反而先进入
了环形队列中。
可 以 看 到 , 在 上 面 源 码 中 的 <1> 处 , 在
Flux.just("Hello","DockerX")中两个元素下发完毕,这时会调用
topicProcessor 的 onComplete 方 法 , TERMINATED 会 被 设 定 为
SHUTDOWN,topicProcessor成为冷数据源。当后面的ZERO、One、Two
产生订阅的时候,alive就会得到false,也就进入了下面源码所示的
冷数据源的范畴。
同 时 , 我 们 可 以 看 到 , 在 上 面 的
topicProcessor_coldSource_test 测 试 代 码 中 , 调 用 了
topicProcessor.onNext方法下发xxx、aaa、bbb,通过观察输出,可
以发现在上游订阅结束之后,我们依然能通过topicProcessor.onNext
方法往环形队列中添加元素,所以此处算是一处隐藏的Bug,大家在开
发中需要注意。
再看看topicProcessor_coldSource_test中<2>处的代码,可以发
现后续并没有产生输出,此时需要注意,在<1>处订阅的上游源生产者
结束下发元素后,所调用topicProcessor的onComplete方法会将上游
的Subscription置空,所以TopicProcessor.onSubscribe方法中调用
的 Operators.validate ( upstreamSubscription , s ) 依 然 会 返 回
true,也就是会进入EventLoopProcessor#requestTask方法中,<2>
处的代码不产生输出问题的症结应该就在此方法中。
需要注意,在调用了topicProcessor的onComplete方法后,需要
将TERMINATED设定为SHUTDOWN,在下次topicProcessor重新订阅上游
源 的 时 候 , TERMINATED 依 然 为 SHUTDOWN 。 在 产 生 订 阅 之 后 ,
topicProcessor 会 向 上 游 发 出 请 求 , 这 时 会 调 用 TopicProcessor #
requestTask方法,其内部会调用TopicProcessor#run方法(具体为
在requestTask方法中调用parent.run),由其中的源码可知,这里会
抛出一个AlertException.INSTANCE异常。在RequestTask#run中,会
通过try...catch捕获该异常并经过WaitStrategy.isAlert(t)判断
为 true 。 从 前 面 分 析 的 整 个 过 程 来 看 , 我 们 并 没 有 看 到
parent.cancelled ( 即 EventLoopProcessor # cancelled ) 被 设 定 为
true的操作,所以此处直接返回。
但是这里有一点要注意,在调用TopicProcessor#requestTask方
法时,会将TopicProcessor#minimum字段的值设定为当前的游标值
minimum.set(ringBuffer.getCursor())。于是TopicProcessor#
subscribe的coldSource(...)方法内传入的是当前的RingBuffer游
标所在的序列号。由上面EventLoopProcessor#coldSource的源码可
知,由start::getAsLong得到的就是当前游标所在的序列号,在执
行+1操作后,s>ringBuffer.getCursor条件成立,即会调用订阅者的
onComplete方法,所以可以看到Demo的执行结果是XXXXX。注意,如果
至始至终只有一个上游源生产者,那么在上游源结束下发元素后,
TopicProcessor#minimum字段的值并不会再发生变化,这也是冷数据
源在此处可以合理做元素下发工作的前提保证。相关源码如下:
回头再看看TopicProcessor.onSubscribe方法。这个方法首先会
进行Operators.validate(upstreamSubscription,s)判断,之前讲
解过,当该TopicProcessor在初始源存在的基础上同时又订阅了一个
新源,此时会调用新源关联的Subscription的cancel方法,表示可以
取消元素的下发动作,接着直接返回false。这告诉我们,同一时刻,
TopicProcessor只能订阅一个上游源,所以对于TopicProcessor的使
用,我们更多地会通过调用其onNext方法来实现多个生产源的效果,
后面将通过Demo来讲解其正确的用法。
再次回到TopicProcessor#subscribe方法中,跳过对冷数据源的
判断,将Subscriber包装一下,方便其在一个线程池里并发执行任
务,当TopicProcessor有多个订阅者的时候,每个订阅者都位于一个
单 独 的 线 程 中 执 行 自 己 的 任 务 , 此 处 的 设 计 类 似 于 Netty 的
WorkerEventLoopGroup,下面观察一下其中涉及的源码:
可以看到,在对传入的订阅者进行包装后得到了TopicInner对
象 , 在 TopicProcessor 中 加 入 对 订 阅 者 进 行 管 理 的 方 法
incrementSubscribers:

先获取SUBSCRIBER_COUNT的值,再加1,也就是返回先前获取到的
值,当SUBSCRIBER_COUNT全等于0的时候,说明所传入的这个订阅者是
第一个参与订阅的消费者,那么直接设定该消费者开始读的元素所在
的环形队列的参考序列号为minimum.getAsLong(默认值是-1,也就是
会读下一个数字0的位置,所以称之为参考,-1代表没有元素入环,0
代表元素进入第一个位置,环形队列是数组形式的)。接着就将此参
考序列号加入限制序列数组中。
若SUBSCRIBER_COUNT的值不等于0,说明在新加入这个订阅者之前
已 有 一 定 数 量 的 订 阅 者 订 阅 了 TopicProcessor 。 那 么 就 获 取 当 前
RingBuffer游标所在的位置,并将其作为这个新加入的订阅者读取环
形队列中元素的参考位置(也就是只读参与订阅之后新入环的元素,
即游标所指的下一个位置),并将此位置加入限制序列数组中。
最后就是在executor中执行TopicInner对象包含的任务,这里的
TopicInner需要实现Runnable接口:
下 面 将 分 两 部 分 来 讲 解 TopicInner # run 的 实 现 。 首 先 , 从
TopicProcessor#subscribe可以知道,TopicInner构造器传入的参数
对 象 pendingRequest=RingBuffer.newSequence ( 0 ) 返 回 的 是 一 个
UnsafeSequence 对 象 , 该 对 象 内 value 的 初 始 值 为 0 。 然 后 , 从
TopicProcessor 的 构 造 器 可 以 知 道 ,
this.barrier=ringBuffer.newReader调用的是UnsafeRingBuffer中的
newReader,即sequenceProducer.newBarrier:

结合之前用于展示订阅者消费元素的图6-2,下面通过实际操作带
着 大 家 再 回 顾 一 下 整 个 过 程 , 可 以 看 看
EventLoopProcessor.waitRequestOrTerminalEvent方法:
顾名思义,pendingRequest就是待处理请求,此变量的写操作总
共有两处,一处是在TopicInner#request方法内,另一处会在后面提
及 。 当 TopicProcessor 与 下 游 订 阅 者 产 生 订 阅 关 系 的 时 候 , 在
TopicInner#run方法内调用subscriber.onSubscribe(this),这里
的this就是TopicInner对象(充当Subscription角色)。由此可以知
道,接下来会调用TopicInner#request,其内会执行addCap方法,即
通过原子类的方式实现pendingRequest=pendingRequest+n的语义。
在 TopicProcessor 与 下 游 订 阅 者 产 生 订 阅 关 系 的 时 候 , 会 在
TopicInner#run方法内调用subscriber.onSubscribe(this),该方
法的执行可能相对于当前线程是异步的。在TopicInner#run方法的执
行 过 程 中 , 可 能 会 首 先 执 行
EventLoopProcessor.waitRequestOrTerminalEvent方法。所以这里要
做的就是等待订阅者发送请求,即设定pendingRequest的值。这也就
对 应 了 TopicInner # request 中 调 用 的 addCap 方 法 , 用 来 跳 出
waitRequestOrTerminalEvent中的while循环。
在EventLoopProcessor.waitRequestOrTerminalEvent方法的执行
过 程 中 , 当 pendingRequest.getAsLong ( ) <=0L 成 立 时 , 会 调 用
barrier.waitFor(waitedSequence,waiter)来表示:既然已经在等
待下游订阅者请求元素这个需求了,索性就再多判断一下,如果下游
有 请 求 , 那 么 就 读 取 RingBuffer 中 对 应 位 置 的 元 素 , 即
waitedSequence代表的位置,结合前面对上游源生产者下发元素时所
调 用 的 EventLoopProcessor # onNext 方 法 中 的 ringBuffer.next 的 讲
解,会在ringBuffer.next中设定下一个可消费的游标,当该游标值大
于 或 等 于 waitedSequence 值 的 时 候 , 订 阅 者 才 有 资 格 消 费
waitedSequence指向的RingBuffer中对应位置的元素,以此来降低后
续的计算资源消耗。
当上游开始下发元素到环形队列中时,每个TopicInner对象都开
始等待自己要读的那个序列号位置有元素填充的通知。而由上面
TopicInner 中 的 字 段 定 义 可 以 知 道 ,
sequence=RingBuffer.newSequence ( RingBuffer.INITIAL_CURSOR_VA
LUE),然后在下游订阅者与TopicProcessor产生订阅关系的时候,即
在 调 用 TopicProcessor # subscribe 时 , 会 调 用
signalProcessor.sequence.set方法来设定该下游订阅者读取的初始
元 素 在 RingBuffer 中 的 位 置 。 如 何 判 断 该 位 置 ? 首 先 用
incrementSubscribers来判断该订阅者是否是第一个参与订阅的订阅
者,然后对下面两种情况分别进行处理。
◎ 若该订阅者是第一个参与订阅的,那么序列号的初始值为-1,
因为RingBuffer是以数组的形式存储Slot的,所以数组的第
一个元素的序号为0,于是该订阅者开始读元素的位置就是
0。
◎ 若该订阅者并非第一个参与订阅的,则获取当前游标位置的下
一个位置作为其开始读元素的位置。所以这里的
barrier.waitFor(waitedSequence,waiter)等待的是上游
将其所需的第一个元素填充到相应的位置。
需要注意:TopicInner实现了Runnable接口,所以它会作为一个
任务放入线程池中执行,在TopicProcessor#subscribe方法中,在设
定完TopicInner对象的sequence之后,就可以提交该任务了,即执行
executor.execute(signalProcessor)。
接 下 来 看 看 TopicInner # run 方 法 内 的 循 环 ( 如 下 面 的 源 码 所
示 ) , 除 了 上 面 一 段 介 绍 的 主 要 逻 辑 ( EventLoopProcessor #
waitRequestOrTerminalEvent 中 的 循 环 ) , 这 里 再 说 说
pendingRequest 带 来 的 一 些 控 制 逻 辑 。 判 断 TopicInner 包 装 的
Subscriber,该订阅者请求元素的数量是否等于Long.MAX_VALUE,假
如 其 默 认 值 是 LambdaSubscriber , 那 么 就 调 用
s.request(Long.MAX_VALUE),前面也讲过BaseSubscriber的实现,
可以自行定义一个Subscriber,那么元素请求数量就可以发生改变。
在这里,我们保持默认配置即可,即unbounded为true,也就是说,如
果下游订阅者的需求不断,上游只要有元素,尽情下发即可(可以认
为元素请求数量无限)。
于是,首先拿到的是availableSequence,当订阅者获取的下一个
Sequence位置(即nextSequence)小于或等于这个序列位置的时候,
核 心 业 务 就 是 从 RingBuffer 中 获 取 并 下 发 元 素 的 , 即
subscriber.onNext(event.value),然后读取下一个序列并循环下
发元素。当unbounded为false时,就需要判断订阅者的元素请求数量
了,这里是pendingRequest的另一处变量写操作,当元素请求数量可
以自定义的时候(即元素请求数量为有限个时),每一次下发元素都
要对元素请求数量执行减1操作,即getAndSub(pendingRequest,
1L),当其数值减至0时,停止下发元素,线程要么结束跳出,要么就
挂起1s,循环判断逻辑,直到跳出。
当while(true)循环执行结束的时候,即该执行线程执行逻辑即
将终了,记得要从RingBuffer的控制限制队列中删除当前这个线程所
操作的订阅者所属的序列,同时将processor所记录的订阅者数量减
1,并且此TopicInner对象会将自己的running状态设定为false(即一
个 Boolean 原 子 类 ) 。 因 为 执 行 了 移 除 限 制 序 列 的 操 作
( removeGatingSequence ) , 所 以 就 要 调 用 一 次
signalAllWhenBlocking , 这 里 与 之 前
EventLoopProcessor.RequestTask#run中循环内的操作呼应上了。也
是因为RequestTask#run内使用的是readWait等待方法,所以从这里
就可以看出两个EventLoop之间的联系,希望大家以后接触这种类型的
设计时(比如Netty的两个EventLoopGroup)可以有更好的理解。
在 这 里 , Reader 的 默 认 策 略 是
WaitStrategy.phasedOffLiteLock , 但 底 层 等 待 策 略 也 是
WaitStrategy.liteBlocking,与readWait定义所使用的策略一样。虽
然 它 们 都 调 用 了 同 一 个 静 态 方 法 , 但 其 返 回 的 是 通 过 new
LiteBlocking得到的两个不一样的对象,这样在上游源生产者下发元
素与下游订阅者消费元素对RingBuffer进行相关操作时,不会因为同
一把锁而造成业务耦合。所以务必把情况区分开来,不要犯低级错误
(认为操作的是同一个东西)。
至此,关于TopicProcessor的核心内容都已经讲解完毕,最后通
过 一 个 Demo 来 展 示 在 有 多 个 元 素 生 产 线 的 情 况 下 该 如 何 使 用
TopicProcessor。假如不是在单生产者内定义并发逻辑来生产元素
的,那么我们完全可以抛弃上游源生产者的订阅关系,直接在多个线
程中调用TopicProcessor的onNext方法,反正在API内部也有针对环形
队列的控制方法。由前面所讲可知,其中的barrier和readWait是一种
解耦关系,所以我们可以放心使用。
在这里,将分别以两种方式给大家展示实现过程。第1种方式,在
单生产者内并发生产元素:

源码中使用了sink来产生订阅关系,然后通过设定一个多线程执
行器来并发生产元素并下发,其执行结果如下:
如果只有一个生产者,请务必像源码中这样处理,这样我们自己
可以更容易地进行操作和控制。
第2种方式,抛弃生产者,直接调用其onNext方法,我们对上面的
源码进行了如下改造:
两段代码的执行结果一样,所以在实际生产活动中,读者可以根
据不同情况选择不同的方式。

6.7 小结
Reactor 3 对 Disruptor 并 发 框 架 库 的 第 三 方 实 现 与
TopicProcessor对类EventLoopGroup设计方法的借鉴,都是大家在开
发基础库和一些框架时应该掌握的方法,即善于借鉴他人优秀的设计
理念,但又不限于其框架的束缚。在第7章中,我们将接触与
TopicProcessor比较相似的WorkQueueProcessor,会通过探索其细节
来查看两者的区别。

[1] 扇入:是指直接调用模块的上级模块的个数,扇入大表示模块的复用程度高。扇出:是指模块直接调用
的下级模块的个数,扇出大表示模块的复杂度高,需要控制和协调的下级模块多。本书不会过多涉及这两个
概念,大家要了解的话,可自行在网上查找相关资料。
第7章 对WorkQueueProcessor的解读
通 过 查 看 WorkQueueProcessor 类 的 注 释 可 知 ,
WorkQueueProcessor与TopicProcessor非常相似,但它仅部分遵守了
Reactive Streams规范。该Processor会将上游源下发的元素仅分配给
下游订阅者中的一个,所有订阅者共享同一份元素请求的数量需求。
但是它不能保证元素向下游分发会始终按照订阅者的订阅顺序依次循
环分配。
上面这么说可能理解起来有点困难,下面进行简单的解释。
◎ 与TopicProcessor一样,WorkQueueProcessor也是一个异步的
Processor,当将shared设置为true的时候,其支持多个元素
生产线程并发下发元素。
◎ 同样的,WorkQueueProcessor使用了RingBuffer数据结构来推
送数据。
◎ 在 一 定 的 条 件 下 , WorkQueueProcessor 并 不 会 对 每 一 个
subscriber创建一个线程,因此其比TopicProcessor的伸缩
性 更 好 。 能 够 支 持 的 subscriber 的 最 大 个 数 由 线 程 池
executor 决 定 , 但 是 需 要 注 意 , 最 好 不 要 给
WorkQueueProcessor 添 加 过 多 的 subscriber , 这 样 会 增 加
RingBuffer内限制序列的个数,也就变相地增加了各个消费
线程等待挂起与执行的切换次数。在这里,最好使用
ThreadPoolExecutor或者ForkJoinPool,Processor可以检测
它们的容量并在订阅者过多的时候抛出异常。
◎ 也正是因为WorkQueueProcessor不遵循Reactive Streams规范
(即每个订阅者都会消费所有的下发元素),所以其比
TopicProcessor消耗的资源更少。在这里,所有subscriber
的元素请求数量会被累加在一起,然后WorkQueueProcessor
每次只会给其中一个subscriber推送数据,也就是所有下游
订阅者消费下发元素的数量总和与上游Publisher发布元素的
数量总和相等。与TopicProcessor的fan-out广播模式相比,
WorkQueueProcessor中的模式更类似于round-robin模式,但
不保证是公平的round-robin模式(因为下游订阅者的消费速
度可能不同,线程的挂起和唤醒情况也需要考虑,订阅者数
量越多,挂起的线程数量也就越多,当这些线程接收到唤醒
请求的时候,多个订阅者(每个线程代表一个订阅者)可能
会等待同一个位置的元素,这时就产生了冲突,读取元素的
顺序自然也就无法保证,这样会造成某个订阅者连续拿到多
个下发元素的情况,要知道订阅者都是通过游标配合序列并
结 合 CAS 操 作 来 获 取 RingBuffer 中 的 元 素 的 , 在
WorkQueueProcessor的requestTask方法中并不会对当前所读
的游标进行写入序列号的限制)。
接下来,通过一个Demo来给大家展示一下:

执行结果如下:
IntStream.range(1,20)中包含了1~19的元素,结合上面所
述,来理解一下为什么出现了3个Three(不保证公平的round-robin模
式)。
在本章中将通过关键源码对上面的问题细节进行解读,与
TopicProcessor相似的地方就不赘述了,读者可以回顾之前学习的内
容。

7.1 WorkQueueProcessor的requestTask
前 面 已 经 介 绍 过 EventLoopProcessor # onSubscribe , 下 面 将
WorkQueueProcessor作为其子类,即调用它的requestTask方法:

与TopicProcessor相比,这里并不会获取当前游标或做写入序列
号的限制,只需获取下游相应订阅者在订阅时写入的限制序列号中最
小的那个即可。大家可以回顾一下EventLoopProcessor.RequestTask
#run的具体内容,因为postWaitCallback传入的是null,所以无须对
当前游标进行最小限定序列号minimum的设定,这也就造成了每次下发
元素到环形队列的行为只需要顾及下游订阅者中最小的限制序列号即
可,这也就导致可能有多个下游订阅者被挂起、等待同一个序列元
素。在解除挂起信号的时候,其中一个订阅者可以多次反复抢到读取
权限(通过CAS操作),这就是为什么同一个订阅者可以多次抢到下发
元素进行消费的原因。
上游在调用WorkQueueProcessor的onNext方法的时候,在获取要
写入环形队列的序列号时,会调用RingBuffer的next方法,结合之前
章 节 的 讲 解 , 最 终 会 在
reactor.core.publisher.MultiProducerRingBuffer#next(int)内
( 其 他 类 型 的 RingBuffer # next 同 样 如 此 ) 调 用
gatingSequenceCache.set(gatingSequence),通过将当前游标值与
当前获得的最小限制序列号进行比较,取两者较小的进行设定。也就
是在当前游标值等于cachedGatingSequence值或上游元素还未将环形
队 列 存 满 一 圈 时 , 不 会 进 入 if 判 断 语 句 块 , 而 会 返 回 下 一 个 可 往
RingBuffer 中 存 储 元 素 的 位 置 , 同 时 onNext 方 法 会 执 行
ringBuffer.publish(seqId),对外宣称该位置元素可以读取了。具
体 来 说 , 即 对 这 个 新 加 入 的 元 素 序 列 号 执 行
setAvailable(sequence),并唤醒barrier.waitFor中的下游订阅
者。
反观下游订阅者,在与它相关的元素消费下发逻辑
WorkQueueProcessor.WorkQueueInner # run 中 , 通 过
nextSequence=processor.workSequence.getAsLong ( ) +1L 可 知 , 下
游 各 个 订 阅 者 共 享 了 WorkQueueProcessor 的 workSequence 。 再 结 合
WorkQueueInner # run 中 的 sequence.set ( nextSequence-1L ) , 和
TopicProcessor相比,每个下游订阅者每次获取的nextSequence并不
是 彼 此 独 立 管 理 的 , 而 是 要 依 靠 这 个 共 享 的 workSequence , 即
WorkQueueInner的sequence会随着workSequence的改变而改变,没有
独立性。
sequence字段代表此WorkQueueInner对象当前消费到的序列处。
因为这些订阅者共享了同一个RingBuffer中的元素消费进度,所以多
个订阅者同样会在此通过CAS操作来确定自己下发元素的权限,即执行
processor.workSequence.compareAndSet(...),若CAS操作失败,
则获取当前的workSequence,并再次执行do...while逻辑(会在这期
间 重 复 调 用 sequence.set , 用 于 同 步 当 前 workSequence 的 消 费 进
度)。接着就是正常下发元素的代码逻辑了,unbounded与之前的
TopicProcessor是一样的,此处不再解释。
TopicProcessor会在下发完可用序列后设定限制为返回的当前可
用 序 列 号 availableSequence :
sequence.set(availableSequence)。各个订阅者间并不共享同一个
消费进度,而是独享自己的消费进度,这也是与WorkQueueProcessor
不同之处。
请读者对照下面的源码仔细理解上面所讲的内容:
7.2 WorkQueueProcessor的subscribe
与TopicProcessor相比,WorkQueueProcessor的subscribe逻辑就
简单多了,同样是先判断是否是冷数据源,冷数据源的出现有3种情
况,一种情况是上游元素下发完毕,一种情况是下游订阅者自行解除
订 阅 ( 每 一 个 下 游 订 阅 者 解 除 订 阅 时 都 会 调 用
processor.decrementSubscribers ) , 最 后 一 种 情 况 是
WorkQueueProcessor自己强行停止(即调用dispose方法,其内部会下
发一个onError事件)。请查看如下源码:
关于冷数据源的判定,此处可作为之前内容的补充。我们通过查
看EventLoopProcessor的subscribe方法可知(如下面的源码所示),
如果是冷数据源,那么就按照与TopicProcessor一样的处理逻辑进行
处理;如果不是冷数据源,就对传入的订阅者进行WorkQueueInner包
装 , 然 后 将 下 游 订 阅 者 数 量 加 1 , 接 着 通 过 WorkQueueProcessor 的
workSequence获取当前消费的序列号并将其设定到该WorkQueueInner
对象的sequence中,最后将此sequence加入RingBuffer的限制序列集
合中。
接下来需要控制订阅者的数量,其由WorkQueueProcessor中定义
的字段的executor值决定(可以通过WorkQueueProcessor.Builder来
自定义),如果这里传入的executor类型是ThreadPoolExecutor或者
ForkJoinPool,就可以设定下游最大订阅者数量,同时其也是对资源
处 理 类 型 和 计 算 处 理 类 型 的 设 定 。 若 传 入 的 executor 类 型 不 是
ThreadPoolExecutor或者ForkJoinPool,那么就直接在executor内执
行充当Runnable角色的WorkQueueInner所定义的run方法,这也是7.1
节涉及的部分内容。
最后,通过两个WorkQueueProcessor的示意图来回顾一下前面的
内 容 , 同 时 与 TopicProcessor 进 行 对 比 。 通 过
WorkQueueProcessor.create创建的实例的运行过程示例图如图7-1所
示。

图7-1
通过WorkQueueProcessor.share创建的实例的运行过程示例图如
图7-2所示。可以看到,当其中一个订阅者消费下发元素出错时,会重
新将该元素交给正常的订阅者进行消费。若订阅者消费下发元素出
错,那么绝对会在WorkQueueProcessor.WorkQueueInner#run中抛出
异常;同时,若订阅者调用它的onNext方法时出现异常,也会抛出异
常。所以在WorkQueueInner#run中会对该异常进行捕获,判断并调用
reschedule ( event ) 方 法 重 新 将 该 元 素 加 入 一 个 由
WorkQueueProcessor定义的异常元素消费队列claimedDisposed中。最
后交由正常运行的WorkQueueInner对象中的waiter所定义的逻辑,从
该claimedDisposed队列中将该元素下发并消费。

图7-2

7.3 冷热数据源的区别
在本节中,会对冷热数据源进行一些探讨。
当Publisher是冷数据源的时候,能确定的是在每产生一次订阅关
系时,都会重新产生元素,没有订阅则不会生产数据。而热数据源会
在没产生订阅关系的情况下,提前产生元素。
举个例子,Reactor中最常见的热数据源操作就是just,从Flux#
just的定义可知,它需要一个确定的元素或一个包含多个元素的数
组,然后在每产生一次订阅关系的时候就重新将这个元素或数组下发
一遍。也就是说,当我们涉及的场景要重用一个HTTP调用时,首先会
得到这个HTTP调用的结果,然后当有订阅者产生订阅关系的时候,就
将这个结果下发,无论后续有多少个订阅者,它们始终都会获得同一
个结果(因为下发元素在订阅之前就已经产生了)。
而如果是一个冷数据源,在每产生一次订阅关系时就要发生一次
HTTP调用,即在需要产生订阅关系的时候,都要发生一次HTTP调用,
然后将新得到的值下发给这次的订阅者,这就符合我们的现实需求
了。
下面针对这种情况举个例子,此处传入的参数可以是
httpclient("xxx"),即会有Flux.just(httpclient("xxx")),
其中的httpclient("xxx")方法会立即产生一个返回值,当返回值是
一个元素时,在FluxJust<T>构造函数内将其赋值给其中定义的final
T value变量,即在创建Flux实例的时候,无论订阅与否,上游源下发
的元素已经确定,并不会每产生一次订阅关系就重新产生一个元素或
数组。
为了得到我们想要的效果,参数应该是一个函数式动作,即()-
>httpclient ( "xxx" ) , 然 后 在 产 生 订 阅 关 系 的 时 候 , 得 到
httpclient("xxx"),这也就意味着得到结果元素,然后执行下发操
作 。 若 返 回 的 是 包 含 多 个 元 素 的 结 果 , 则 可 以 执 行 Flux 的
defer(Supplier<?extends Publisher<T>>supplier)操作;若返回
的 是 只 包 含 一 个 元 素 结 果 , 则 执 行 defer ( Supplier< ? extends
Mono< ? extends T>>supplier ) 操 作 , 将 FluxJust 的 实 例 化 操 作 推
迟,传参为()->Flux.just(httpclient("xxx"))。有了这个函
数动作,对于订阅者来说,上游源生产者就是一个冷数据源。

7.4 实例详解
下面先来看一个关于冷数据源的Demo:
执行结果如下:

通过图7-3来展示上面Demo的执行过程。
图7-3
可以看到,无论有几个订阅者,都会得到图7-3左上角方框中所有
小圆代表的元素。
对于热数据源,其更常见于Processor的使用过程中,结合前面所
学,下面通过一个EmitterProcessor来进行展示:
执行结果如下:

在这里,先抛开冷热数据源,借这个例子,我们先对第5章的内容
进行实战。在5.3节中对EmitterProcessor的源码进行过解读,其内部
使用了类似于publish的方式来进行多订阅者的管理。从订阅者的角度
来看,订阅者彼此之间是相互独立的,虽然实际上它们共有一个订阅
关系,但得到的结果是不同的。从这个角度来说,是不是可以将
UnicastProcessor改造一下,一般它不可以同时拥有多个订阅者,那
么是不是可以通过Flux.publish来对其订阅者进行管理,然后通过
autoConnect操作来实现下游订阅者接收到的元素是其进行订阅时上游
源生产者正在生产、下发的元素的效果:
执行结果如下:

这里除了多增加了一个map转换加工操作外,其他效果是一样的。
同时,我们通过图7-4来展示热数据源的操作过程。
图7-4
从图7-4可以很直观地看到,上游源生产者的4个元素在publish操
作处分开了,订阅关系产生有先有后,生产者并不会因后产生的订阅
者而重新生成数据并下发,生产者自始至终只执行了一次生成数据的
过程,相信大家都已经清楚其内在实现原理了。而在图7-3中,上游源
下发的元素始终在一个方框内,每产生一个订阅关系都会重新生成一
份数据。

7.5 小结
终于将WorkQueueProcessor所涉及的知识点讲解完毕,我其实还
提供了一个比较完整的Demo,因篇幅问题,就不放在书中了,大家可
以 参 考 本 书 Reactor 源 码 包 中 的
com.dockerx.demoreactor.WorkQueueProcessorTest测试代码。
另外,在本章的最后,我们对冷热数据源的内容进行了探讨,相
信大家已经对它们有所了解。在第8章中,将会讨论Reactor中特别提
供的Context。
第8章 Reactor中特供的Context
在 本 书 第 2 章 2.1.1 节 中 , 我 们 已 经 涉 及 了 Context , 它 是 从
Reactor 3.1.0版本开始加入的。本章将对它进行专门的探索,以帮助
我们在实际的生产活动中更好地应用它,Spring Framework 5.2中的
响应式事务也是基于它实现的。

8.1 Context的设计缘由
一路走来,通过对源码的探索可知,Reactor中有大量针对并发操
作的应用,不仅是原子类,还包括其他数据结构的设计,如无界队
列、环形队列的设计应用等,以及对线程的切换调度。这些设计无形
中都增加了我们掌握Reactor的难度。同样,本章也是介绍响应式操作
的,主要介绍线程里上下文中的数据传递。
根据之前所讲解的内容可以知道,在响应式编程中,一个线程很
可能被用于处理多个异步订阅关系,同样,一个订阅关系在元素下发
的过程中往往也可能从一个线程切换到另一个线程。下面通过前面讲
过的一个Demo来进行说明:
部分执行结果如下:

可以看到,在一次订阅内,通过调度,对元素的消费切换了多个
线程。这样也会带来一个问题。在非响应式开发的项目中,我们往往
会在一个线程内将任务处理完,也就是说我们可能会在线程的上下文
中存储一些对象数据,方便我们在整个执行过程中使用它们,同时,
在线程生命周期结束时会将所存储的数据销毁。也就是说,开发比较
依赖于线程模型的特性,比如ThreadLocal可以使你在一个线程中共享
数据。而在响应式开发项目中,这样就有点不合适了。假如这时还要
强行使用ThreadLocal来共享数据,那么很可能会出现意想不到的状
况。因为有时候会自定义线程池,在调度的时候几个订阅关系可能会
共用一个线程池,所以强行使用ThreadLocal可能带来潜在的数据泄露
危险。
也就是说,假如订阅关系A在ThreadLocal中存储了一个键值对
<a,b>,然后恰好运行订阅关系A的线程切换到运行订阅关系B的线程
了,那么在订阅关系B中也可以通过键a获取值b,这就造成了订阅关系
A的资源泄露。
正因为如此,从Reactor 3.1.0版本开始,Reactor给我们提供了
ContextAPI来解决此类问题,它与ThreadLocal的功能相似,专门用于
服务Flux或者Mono单个订阅关系的上下文存储。

8.2 对Context的解读
在解读Context之前,下面先来看一个Demo:

执行结果如下:

为什么会出现上面的结果,其实大家若是从本书开始读到这里,
就不会觉得难以理解。因为在产生订阅关系的时候,订阅者由后往前
一层层被中间操作包装,所以会先执行subscriberContext操作涉及的
subscribe方法:
由上面的源码可以看出,首先执行传入的doOnContext函数,然后
结合2.1.1节中涉及的Context内容可知,在产生订阅关系时,Context
针对的是当前订阅者,并不会发生与其他订阅者公用Context的情况,
这样也就保证了数据的安全性。
在执行完doOnContext函数后,再将得到的Context对象与传入的
订阅者包装出一个新的订阅者ContextStartSubscriber,在这个订阅
者内重写currentContext方法,使其返回传入之前得到的c。
结合上述内容来看ContextStartSubscriber中的定义:
再次回到contextSimple1这个Demo中,先执行<1>处的上下文操
作,也就是添加了一个以“message”为key,以“World”为value的
键值对,假如Context对象内已经有一个键值对了,那么就结合之前存
在的键值对重新创建一个ContextX对象,其中X为键值对的个数,当X
超过5时,直接将其命名为ContextN,这也是典型的Tuple类型的操
作。然后执行<2>处的flatMap操作和<3>处的操作,这里要搞清楚整个
执行过程的先后顺序,Publisher#subscribe总是先人一步,然后才
执行Subscriber#onSubscribe、Subscription#request,接着上游
源才会调用订阅者的onNext方法,这对于理解订阅关系操作链的执行
过程特别重要。订阅整个关系操作链之后,就是通过subscribe方法由
下游往上游逐层地对订阅者进行包装、包装、再包装。接下来我们来
看看Context的源码:
从上面的Context2#put方法可以清晰地看到,当key相同的时
候,其执行的并不是update操作,而是重新创建了一个对象。由此也
能看到Context具有不可变性。一旦不再使用获得的对象,就可以将其
抛弃,而不是在该对象的基础上修改内部值。其他Context中涉及的
API就不解释了,比较简单,留给读者自行探索。
需要记住,Context是以订阅者为主体来设计的,用于维护整个订
阅过程上下文中产生的额外信息。
结合上面的内容,来看看下面的Demo:

执行结果如下:

之前介绍过产生订阅关系之后整个函数调用链内部操作的执行顺
序,map、flatMap都属于在onNext内执行的我们依据元素所设定的相
关加工动作,也就是说,在做程序设计时,我们会依据上游源下发的
元素来做相关消费逻辑的设计。在这里,因为想要从开始就对Context
执行操作,即按照习惯自上而下执行操作,于是通过<1>处得到下游订
阅者的Context的Mono<Context>,其中涉及的源码如下:
我们之前讲过onAssembly,也讨论过钩子函数hook,大家可以回
顾一下之前的内容。回到contextSimple2这个Demo中,首先下发的是
Context类型的元素,<1>与<2>处的Context内容是不同的,这是因为
我们通过Mono.subscriberContext将订阅者的Context拿来做源的下发
元素,在其(Context)之上执行了一些操作。在整个操作链中,无论
下游如何对Context执行操作,也不会影响上游操作链中Context的内
容,这也是其不可变性的具体体现,同时也保证了整个链式操作的不
可变性。
在 contextSimple2 的 <3> 处 , 获 得 了 一 个 全 新 的
MonoCurrentContext实例,根据flatMap的特性,其根据一个上游元素
来获取新源(也可以叫作子源)并与下游订阅者之间产生订阅关系。
这 里 要 注 意 , 上 游 初 始 源 从 订 阅 者 的 currentContext 中 获 取 到 的
Context使用了Context#put操作,这时会得到一个全新的Context对
象,该对象与<2>处map操作中订阅者的currentContext对象不是同一
个对象,这点千万要注意。所以,flatMap中的子源获取的当前订阅者
的Context对象和下发的这个Context对象元素也就不是同一个。
此 处 要 强 调 一 下 , Context 与 订 阅 者 的 绑 定 特 性 是 由 订 阅 者 的
currentContext方法来决定的,而不是由某个Context对象来决定的。
若 要 认 定 一 个 Context 对 象 专 属 于 某 个 订 阅 者 , 则 必 须 通 过
currentContext方法来将该Context对象与此订阅者绑定在一起。
通过contextSimple2的执行结果,可以看到Context与订阅者的绑
定特性实现了类ThreadLocal效果,只不过ThreadLocal是针对线程来
设定的,而Reactor中实现的Context是针对订阅者来设定的。
最后,通过一个相对实战的Demo来让大家学习在项目中如何使用
Reactor Context:
这个Demo的思路是,当用户通过HTTP访问服务器来执行一个任务
的时候,会通过验证一个关联ID(此ID可以是一个代表权限的ID,也
可以是其他方面的ID)来执行不一样的策略。因为涉及元素的下发动
作,所以这个策略会被放在上游,而这个策略是根据订阅者来制定
的,因此根据不同的订阅者可能会产生不一样的策略。这个策略刚好
会影响元素的下发,那么就要从每一个订阅者手中拿到相应的数据来
进行判断,这时如何获取数据就是我们面临的大问题了。我们知道,
下游与上游是通过onSubscribe在产生订阅关系的时候进行交互的,结
束 订 阅 关 系 则 要 使 用 cancel 方 法 。 在 整 个 订 阅 过 程 中 , 订 阅 者 的
currentContext都与自身绑定,不会被其他订阅关系影响,所以在这
里直接获取订阅者的currentContext,并判断其中的相应条件,而对
于 订 阅 者 存 储 的 这 些 信 息 , 可 以 在 下 游 操 作 链 中 通 过 Mono 的
subscriberContext相关方法将其加入订阅者的currentContext中。
于是,通过上面Demo中<1>处的操作,我们从当前参与的订阅者的
Context中获取了以HTTP_CORRELATION_ID为key的value值,然后根据
该value值来进行策略判断,即传入一个值,通过策略判断来返回一个
全新的值。可以使用Function<T,R>函数来表达上面这个逻辑,其中
可以选用map等操作(此处选择了handle操作),具体操作细节可以回
顾之前关于handle操作的内容。不过需要注意,handle操作所包装的
订 阅 者 的 currentContext 来 自 下 游 传 入 的 订 阅 者 的
currentContext(如下面的源码所示),这样就解除了handle操作如
何传递下游订阅者currentContext的疑惑:
这 里 还 有 一 点 需 要 注 意 , Mono.subscriberContext ( Context
mergeContext ) 与 Mono.subscriberContext ( Function<Context ,
Context> doOnContext ) 的 返 回 值 类 型 都 是 Mono<T> , 而
Mono.subscriberContext的返回值类型是Mono<Context>。其中需要强
调的是方法的定位不同,前两个方法是根据需要在Context中添加键值
对 , 方 便 在 处 理 链 上 游 使 用 , 而 最 后 一 个 方 法
Mono.subscriberContext是在上游获取订阅者currentContext的值的
操作。
于是在测试方法内,通过将数据写入下游订阅者的
currentContext中,实现了上游策略的驱使。拿权限设定来讲,可以
直接使用Context.of(...)方法,把从其他方法处得到的结果放置到
订 阅 者 的 currentContext 中 , 也 可 以 通 过 Function<Context ,
Context>doOnContext这个函数动作先从Context中获取相应权限,然
后根据自身需求修改或重设权限到订阅者的currentContext中。
在上面Demo的最后,我们通过第9章将会涉及的StepVerifier来进
行预期测试。
为了方便大家对比使用不同操作得到相同的结果,下面将map操作
的源码实现展示给大家,修改相应的doPut为doPut0,测试结果并无差
别,所以对于API来说,并不一定非用哪一个,实现我们的目的才是最
重要的。
8.3 小结
本章内容不多,但却是Reactor 3.1.0版本中新增的内容,而且比
较实用,这些内容在开发中可以带给我们很多帮助,因为上下游之间
的交互有时候很必要。另外,通过本章最后一个Demo,希望大家可以
切实地掌握API的选用思路,往往参数类型、方法名、返回值类型就是
一个方法的核心,也是接口定义的核心。在设计层面,重要的不是方
法的实现过程,而是方法的目的,这就好比种瓜得瓜,种豆得豆,而
方法就是“种”这个动作,传入的参数类型就是豆或瓜,返回值类型
也是豆或瓜,凡事都讲究因果。
第9章 Reactor中的测试
本章将主要介绍Reactor中的测试。本章中的Demo并不是标准的测
试Demo,只用于给大家介绍相关的知识点。其实在《Java编程方法
论:响应式RxJava与代码设计实战》一书的实战Demo中提供过标准的
单元测试实现,其形式是when…then…。为了做到这种自动化单元测
试,Reactor给我们提供了专门的reactor-test测试模块。在正式开始
本章内容之前,我们先来配置一下代码环境。在Maven项目中,需要添
加以下依赖:

在这里,reactor-test主要进行了两方面的测试。
◎ 针对上游及通过一系列操作得到的新源的测试:使用
StepVerifier。
◎ 针对自定义的一系列操作(包括Reactor库提供的操作API,通
过compose或transform操作组合出的通用操作)的测试:使
用TestPublisher。

9.1 StepVerifier测试源码解析
本书的目的是给读者讲解一些源码设计上的知识,所以本节将带
着大家深入StepVerifier测试库的源码以了解它的设计实现思路。
9.1.1 接口定义
对源做测试,将从源的创建,元素的下发、结束,以及订阅关系
中的Context等几个方面来进行设计和实现。
因为在每个测试中都会有针对结束操作(包括正常结束和异常结
束)的测试,所以首先定义reactor.test.StepVerifier.LastStep接
口。该接口主要用于验证异常结束或正常结束,如对上游源下发的
error 事 件 进 行 处 理 : StepVerifier
consumeErrorWith ( Consumer<Throwable>consumer ) 。 在 处 理 error
事 件 的 过 程 中 , 如 果 其 中 的 consumer 参 数 报 出 异 常 , 则 会 在 执 行
verify期间(即执行验证操作)接着抛出异常。LastStep接口中具体
的方法设定如下:
测试过程中对下发元素的测试才是我们的核心需求,所以在这里
通过Step接口来聚合这些验证操作,并继承上面定义的LastStep接
口 。 Step 接 口 内 部 提 供 了 consumeNextWith 、 assertNext 、
expectNext ( T t1 , T t2... ) 、 expectNextSequence 、
expectNextMatches等中间操作,我们会在后面应用这个接口的时候具
体讲解它。
接着,在针对上游源生产元素的代码逻辑的测试中,通过之前的
学习可以知道,元素的生产、下发与Subscription密切相关,可以参
考之前解读的InnerProducer接口和FluxSink。另外也可以知道,当支
持QueueSubscription时,其用于表示当前Subscription是否支持异步
请求,对于针对上游源的测试,通过设计一个接口FirstStep并继承
Step 接 口 来 实 现 , 接 口 内 部 提 供 了 expectFusion 、
expectSubscription等方法。
最 后 , 因 为 要 对 订 阅 者 的 Context 进 行 测 试 , 所 以 可 以 关 注
reactor.test.StepVerifier.ContextExpectations接口,其内部包含
了hasKey、hasSize、contains、containsAllOf、matches等方法,后
面会通过应用实例具体讲解。
9.1.2 接口实现
在9.1.1节中,说到了一些针对期望事件而做的接口方法设定,如
果想要利用这些方法来针对订阅过程中产生的各种形式的内容设计一
系列测试并将它们组合在一起,该怎么做?我们可以将每一个测试内
容包装成一个事件,然后将其加入一个容器,这里容器的形式可以是
队列或者列表,最后在验证的时候遍历该容器中所存事件,依次执行
即可。下面来看StepVerifier的create方法:

在这里,对于用于测试的publisher,其充当生产、发布元素的角
色,首先应该使用FirstStep的实现实例,即这里通过StepVerifier#
create方法得到DefaultStepVerifierBuilder实例,该实例的类型为
FirstStep。下面来看看该实现实例内部的一些字段和构造函数的定
义:
可以看到,此处有一个字段,名为script,它是一个列表,其中
包 含 的 就 是 我 们 的 预 期 事 件 。 由
this.script.add(defaultFirstStep())可知,其添加的默认事件
类型是SignalEvent。由于这里的预期事件针对的不仅仅是onNext、
onError、onComplete这些事件,也包含Step接口中涉及的其他事件,
而且这些事件在Reactor中都由reactor.core.publisher.Signal统一
标识,因此SignalEvent就是基于Signal来做的预期处理逻辑包装:
我们在生成SignalEvent实例的时候,可以根据自己的需要来自定
义BiFunction的实现。此处BiFunction中的Signal代表了所下发的事
件动作。Signal接口设计的核心在于其默认实现的accept方法:
此方法内的逻辑很清晰,主要是关于订阅者接口设计的几个方
法,整个下发过程中所有的逻辑都是围绕着这几个方法来进行的,而
我们在本书前面所学的操作设计也是围绕着订阅者进行的,这是一个
最基本的设计思路。在这里,首先判断该订阅中发生的事件是否为
OnNext,是的话就下发得到的值(通过get方法):
通过Signal#next可知,可以得到ImmutableSignal对象,那么在
上面源码中的Signal#isOnNext中调用getType所得到的结果就是创建
ImmutableSignal 对 象 时 所 传 入 的 参 数 SignalType.ON_NEXT , 通 过
Signal#get方法可以得到上面源码中的参数t。Signal中的其他方法
可以参照此处对比理解。
接着,解读一个LastStep接口中的方法实现,其他实现过程可以
参考此实现过程:

由 上 面 的 源 码 可 以 看 出 , 其 中 主 要 定 义 了 一 个 SignalEvent 的
BiFunction实现实例,在执行该预期事件(SignalEvent)代码逻辑的
时候,调用SignalEvent的test(Signal<T>signal)方法即可,并将
定义的这个SignalEvent实例加入this.script的列表中。
在 new SignalEvent ( BiFunction , String ) 传 入 的 这 个
BiFunction函数式实现中,如果下发的不是OnError,而是一个异常,
那 么 就 会 调 用 fail 方 法 , 并 返 回 其 结 果 Optional.of ( new
AssertionError(prefix+String.format(msg,args)+")"))。
若条件都不符合,就返回Optional.empty。整个代码逻辑还是比较简
单的。
9.1.3 验证
由 9.1.2 节 最 后 的 内 容 可 知 , expectError 返 回 的 结 果 类 型 是
DefaultStepVerifier,在设定完执行脚本后,执行就好了。其中主要
的执行过程是,首先获得create处设定的Publisher源,然后对设定的
一系列条件进行包装,得到一个Subscriber,最后产生订阅关系。这
里 主 要 是 通 过 调 用
DefaultStepVerifierBuilder.DefaultStepVerifier # verify 方 法 来
实现的:
如果对本书前面讲解的内容比较熟悉,关于测试代码的实现,还
是很容易看明白的。由于篇幅有限,无法进行更深入、细致的解读,
假如读者对相关内容感兴趣,可以自行探索,也可以观看本书配套视
频中的测试源码解读部分进行拓展学习。接下来,我们将对与测试有
关的具体应用进行讲解。

9.2 StepVerifier测试应用
最常见的测试方法应该是,首先自定义一个源,然后确定该如何
测试其产生订阅时的动作,如我们期望下发过程中可能出现的事件、
元素的值及异常的类型等。在这里,通过一个Demo来展示一下。
首先,定义一个异常事件,即在一个正常源的尾部添加异常事件
代码:

也就是说,我们希望这个Flux在最后产生一个error,同时携带信
息"custom",并在验证代码的时候验证到这一情况。于是,定义如下
测试代码:
◎ 在<1>处,定义了一个Flux源。
◎ 在<2>处,像之前解读的源码那样,创建了一个包装类,用来
包装我们的源和接下来希望测试的脚本动作,此包装类型为
DefaultStepVerifierBuilder。
◎ 在<3>处,通过之前定义的appendCustomError方法,对<1>处
定义的源进行加工和包装,使其最后一个元素为异常对象。
◎ 在<4>处,结合之前的源码解读,我们希望上游源下发的第一
个signal为onNext,下发的期望值为"foo"。
◎ 在<5>处,我们希望上游源下发的最后一个signal为onError,
其内部包含的异常信息为"custom"。之前我在源码解读中专
门 说 过 , expectError 最 后 调 用 了 build 并 返 回 了 一 个
DefaultStepVerifier对象,以供下一步调用verify方法。
◎ 在<6>处,调用verify方法来触发测试。此处代码可有可无,
主要还是为了进行演示,因为expectErrorMessage内部已经
调用过此方法。
这样,我们通过创建一个StepVerifier实现了对自定义源的测
试。而StepVerifier的API比较多,应根据情况选用适当的方法。
当你期望下发的signal为onNext时,如果所下发signal为其他类
型 或 者 onNext 所 携 带 元 素 的 值 不 符 合 我 们 的 期 望 , 就 会 出 现 一 个
AssertionError异常(在这里,我们将expectNext("bar")这一行注
释掉,然后执行,会得到如下结果):

同时,看看其相关实现源码:
可以看到,expectNext最终的验证操作是在addExpectedValue中
执行的。首先会判断下发的signal是否为onNext,若是,则判断下发
的值(通过signal.get获得)是否与自定义的期望值value相同,若相
同就返回Optional.empty。执行期间若有一个判断不成立,就通过
fail方法返回一个AssertionError异常。
◎ 当你下发的signal为onNext且你希望对下发的元素进行一些内
在操作时,例如,下发的元素是一个引用类型对象,如数
组,若要求这个下发数组的长度为8,这时就可以使用
consumeNextWith(Consumer<T>)。下面来看看相关的实现
源码:
首先会在<1>处判断下发的signal是否为onNext,是的话(即确实
为onNext)进入<2>处,对下发的元素进行获取和消费,最后返回
Optional.empty。
◎ 更多的情况是,根据实际情况暂停一段时间(比如热数据源在
下发元素的过程中可能由于I/O因素导致时间间隔不一样,不
一样的时间间隔会得到不一样的元素,因此要根据实际情况
来进行区分),或者先执行另一个任务(任意一个自定义任
务)再继续执行下发元素的任务,那么在这里可以使用
thenAwait(Duration)与then(Runnable)。关于此处的源
码就不多说了,读者感兴趣可以自行探索。
对于expectComplete、expectError及其所有的衍生方法,需要再
次提醒的是,它们内部都调用了verify方法,也就是说,我们无须专
门调用此方法。
同样需要注意的是,verify方法并没有超时的概念,也就是说,
其有可能会永久阻塞。所以,有些时候,我们可以根据实际情况设定
一个执行超时时间,可以将这个超时时间设定为全局使用,也可以使
用verify(Duration)进行指定,全局使用时可以调用StepVerifier
#setDefaultTimeout(Duration)方法:

9.3 操作时间测试
有些源产生元素是有时间限制的,比如某个热数据源,每隔5分钟
产生一个新元素。而在测试过程中,我们不可能花费这么多时间来等
待产生每个元素,此时就需要用到StepVerifier提供的虚拟时钟功能
了。通过StepVerifier.withVirtualTime方法可以创建出使用虚拟时
钟的StepVerifier。
而为了让StepVerifier使用起来更符合我们的习惯,它一般不会
直接接收一个简单的热数据源Flux作为输入,而是接收一个Supplier
函数动作,这样就可以在配置好订阅者之后(即调用verify方法),
延迟创建待测试的Flux,否则,该热数据源Flux会在作为方法参数传
入StepVerifier.withVirtualTime(...)的第一时间就开始执行元素
的生产逻辑,如下:

假如只将Mono.delay(Duration.ofDays(1))作为参数传入,
其会立即执行,这很可能不符合我们的要求。
接下来介绍两种处理期望时间的测试方法,无论是否配置虚拟时
钟均可以使用。
◎ 通过thenAwait(Duration)方法可以让测试过程以Duration
这个虚拟时钟前进,即会暂停验证步骤(允许事件信号延迟
发出)。
◎ 通过expectNoEvent(Duration)方法可以让整个过程持续一
定的时间,期间如果有任何事件信号发出,则表示测试失
败。
不知道大家是否还记得之前将监听器改造为响应式结构的Demo,
即 DemoRecatorFluxTest 中 的 producingCreate 测 试 , 下 面 将 通 过
StepVerifier.withVirtualTime来对其进行测试:
对监听器的响应式结构化的细节解读,大家可以回顾之前的内
容,在这里,只介绍测试部分。
首 先 , 在 <1> 处 , expectSubscription 会 作 为 一 个 事 件 加 入
script , 此 时 一 旦 产 生 订 阅 关 系 , 就 会 执 行
DEFAULT_ONSUBSCRIBE_STEP 中 定 义 的 代 码 逻 辑 , 判 断
signal.isOnSubscribe,如下面的源码所示:
既 然 提 到 了 signal.isOnSubscribe , 那 就 要 关 注 一 下
Subscriber 。 由 verify 方 法 可 知 , 它 在 内 部 实 例 化 了 一 个
DefaultVerifySubscriber 对 象 。 首 先 , 观 察
DefaultVerifySubscriber的构造函数(请看下面的源码),这里会判
断 script 中 的 第 一 个 Event 是 否 为 TaskEvent , 是 的 话 将 其 添 加 进
taskEvents,否则跳出循环。然后,根据taskEvents取出的对象是否
为NoEvent类型来决定this.monitorSignal布尔类型变量的值。
那么可以知道,在产生订阅关系的时候,会先调用订阅者的
onSubscribe方法,从这里可以看到,它会执行onExpectation方法,
也就是说,假如monitorSignal为true,就会抛出AssertionError异
常。
那么若先调用expectSubscription,是否会抛出异常呢?运行一
下我们的Demo,可以发现并没有抛出异常,这到底是怎么回事?我们
如何做才会抛出异常?带着疑问接着看下面的源码:
回 到 DefaultStepVerifierBuilder 的 构 造 器 中 可 以 发 现 ,
this.script 中 默 认 添 加 的 第 一 个 Event 为
DEFAULT_ONSUBSCRIBE_STEP , 其 和 DefaultStepVerifierBuilder #
expectSubscription 中 所 做 的 添 加 动 作 一 样 , 也 会 执 行
this.script.set ( 0 ,
newOnSubscribeStep ( "expectSubscription" ) ) , 该 操 作 存 入
this.script中的对象的类型是SignalEvent,而非TaskEvent。所以通
过this.script.peek得到的Event在进行event instanceof TaskEvent
判断的时候会得到false,直接跳出循环,继而this.monitorSignal的
值就为false,也就是说onExpectation并不会抛出异常。
那么我们Demo的expectNoEvent中又发生了什么?通过下面展示的
源 码 可 以 知 道 , 其 中 当 this.script 的 长 度 为 1 且 第 一 个 Event 为
DEFAULT_ONSUBSCRIBE_STEP时,才会将此NoEvent设定为script中的第
一个元素,这样,在调用订阅者的onSubscribe时就会抛出异常了。
同样,在订阅者DefaultVerifySubscriber的onNext方法中也会调
用onExpectation进行判断,即在expectNoEvent处于等待状态期间,
由 于 expectNoEvent 中 的 NoEvent 会 执 行 其 中 的 run 方 法 , 因 此 会 将
monitorSignal设定为true。若等待期间上游源有元素下发的话,结合
onExpectation实现,可以发现,它会抛出异常,这也就实现了这个方
法的语义:

由此,对我们Demo中的测试代码进行如下修改:
这 时 会 抛 出 java.lang.AssertionError : expectation
failed ( expected no event : onSubscribe ( false ) ) 异 常 , 而
then(...)会通过一个task来执行下发元素的操作,以此给大家展示
它的一个应用场景。而中间的expectNoEvent更多的是测试定时产生元
素的源的场景,来看看下面这个Demo:

正常执行上述测试代码,是完全没问题的,但若打开<1>处的注
释 , 再 次 执 行 测 试 代 码 , 就 会 抛 出 java.lang.AssertionError :
unexpected end during a no-event expectation 异 常 。 虽 然 通 过
take方法只获取了两个元素,但是并不代表Flux.interval不再产生元
素,在expectNoEvent处于等待状态期间,其产生元素并调用take中包
装的FluxTake.TakeSubscriber#onNext方法,也就是产生了onNext事
件,这时抛出异常就是正常的。

9.4 使用StepVerifier进行后置验证
在配置完测试场景最后的期望方法后,还可以使用
verifyThenAssertThat来代替verify触发后置验证。
verifyThenAssertThat 会 返 回 一 个 StepVerifier.Assertions 对
象,可以用其来校验整个测试成功结束后的一些状态(其内部通过之
前介绍过的Hooks钩子函数来实现),其在调用verify前会设定Hooks
的一些实现,在执行verify后就可以获得一个包含这些状态信息的
DefaultStepVerifierAssertions对象:
可以发现,这里涉及了几种收集丢弃元素的事件类型,那么可以
通过下面的Demo来对其使用方法进行展示:
更 多 Demo 请 参 见 本 书 相 关 源 码 文 件
com.dockerx.demoreactor.DemoReactorTest。

9.5 关于Context的测试
关于Reactor中Context的相关知识,若遗忘了的话,可以阅读第8
章进行回顾。
通过前面对reactor.test.StepVerifier.Step接口的解读可知,
其 内 部 定 义 了 与 Context 有 关 的 expectAccessibleContext 和
expectNoAccessibleContext方法。那么在进行测试时,应该如何提前
针 对 Context 进 行 设 定 呢 ? 来 看 看 StepVerifier #
create(Publisher<?extends T>,StepVerifierOptions),可以发
现其第2个参数为StepVerifierOptions类型,这点最终会在verify方
法包装的DefaultVerifySubscriber上体现出来(在第8章中介绍过,
Context是针对订阅者的,因此我们的设定最终也会体现在订阅者身
上):
由上,我们只需要配置出一个StepVerifierOptions对象即可,找
到 reactor.test.StepVerifierOptions 类 的 定 义 , 可 以 通 过 它 的
create 方 法 得 到 这 个 类 的 实 例 , 然 后 通 过 调 用 该 对 象 的
withInitialContext来进行配置。同样,关于操作时间的测试,可以
对调度器进行自定义配置,也可以对订阅者的初始元素请求数量进行
配置:
接下来通过下面这个Demo来理解Context在测试中的相关设定:
其中的Mono.subscriberContext会返回一个包含当前订阅关系中
订阅者Context元素内容的Mono<Context>(若对这里有疑惑的话,看
看第8章的内容),于是就可以进行接下来的测试,通过assertNext来
判断下发的Context类型的元素是否包含指定的值。
假如我们想在正常元素的下发过程中测试Context中包含的内容,
可以使用expectAccessibleContext和expectNoAccessibleContext:
可以看到,这两个方法是不同的,返回值不一样。
expectNoAccessibleContext 的 业 务 比 较 简 单 , 只 需 要 判 断 得 到 的
Context是否为null即可,并不对其中的元素进行内部判断,而且此操
作仅用于当前调用的是CoreSubscriber的onSubscribe方法时,测试
onSubscribe方法所传入的Subscription对象是否也为CoreSubscriber
类 型 , 若 有 , 说 明 存 在 中 间 操 作 ( 比 如 Flux # map 操 作 包 装 的
MapSubscriber 同 时 也 实 现 了 Subscription 接 口 ) , 由
consumeSubscriptionWith进行包装,并加入script中进行管理。
同 样 , 在 expectAccessibleContext 方 法 返 回 的
DefaultContextExpectations实例中,其对各种情况的包装最后都需
要调用then方法来返回主测试线,而then方法也自然包装了我们定义
的测试内容。为了在其中添加多个测试内容,此处借鉴了
java.util.function.Consumer#andThen的用法,希望读者将其作为
重点学习:
可 以 看 到 , DefaultContextExpectations 类 通 过
step.consumeSubscriptionWith方法将它的消费逻辑衔接了起来,使
之回到step所代表的主测试线。这与Consumer#andThen有异曲同工之
妙 。 同 样 , DefaultContextExpectations 类 内 部 的 其 他 方 法 , 如
contains、hasSize、hasKey的内部都是利用Consumer#andThen的衔
接设计来实现的。我们在编写代码时也可以做到对多个条件分別进行
包装、连接,以方便重用代码,也可以进行如下设定:
以上为使用Kotlin语言编写的一段项目代码,其想要表达的是,
<1>处可以通过多个andThen方法,在执行source.close之前执行类似
于finally语句块中的多个操作。
同样,也可以利用java.util.function.Function中的andThen方
法实现通过多个条件过滤元素的操作,这也是andThen方法的核心用
法。
最后,通过两个Demo来展示对普通元素进行下发情况下的Context
的测试:
需要注意,expectAccessibleContext要与then成对出现。

9.6 使用TestPublisher对自定义中间操作进行测试
TestPublisher 通 常 用 于 热 数 据 源 场 景 , 其 实 它 有 点 类 似 于
FluxSink,可以通过调用next方法下发元素,这样我们就可以根据实
际情况来进行测试。
TestPublisher 属 于 抽 象 类 , 它 实 现 了 Publisher<T> 、
PublisherProbe<T> 两 个 接 口 。 在 本 节 中 , 我 们 将 只 关 注
TestPublisher作为Publisher的功能,而在9.7节中会具体深入其作为
PublisherProbe的功能。
结合其默认实现类DefaultTestPublisher,下面对它的API进行相
应的解读。
n
◎ next(T)及next(T,T...),发出1~ 个onNext信号。

从上述源码可以看到,对于元素的下发,这里有一个用于遍历的
subscribers,也就是说,针对此源,它允许同时管理多个订阅关系,
于 是 我 们 来 观 察 reactor.test.publisher.DefaultTestPublisher #
subscribe:

可以看到,当产生订阅关系的时候,将订阅者与源用
TestPublisherSubscription包装起来,即将两者进行绑定,然后每产
生一个订阅关系,就将此TestPublisherSubscription对象加入一个数
组中。结合next方法,可以知道,这是一个天然的热数据源设计,即
并不会在产生订阅关系的时候从头下发之前的元素,而只能获取订阅
之后上游源下发的元素。
◎ emit(T...)与next起同样的作用,并且会执行complete。
emit与next的区别就是会在下发完元素之后执行complete。
◎ complete会发出终止信号onComplete。
◎ error(Throwable)会发出异常信号onError。
接着观察TestPublisher实例的获取方式,我们往往会调用它的
create方法:

由 此 , 使 用 create 工 厂 方 法 就 可 以 得 到 一 个 正 常 的
TestPublisher。而使用createNonCompliant工厂方法,可以创建一个
“ 特 殊 功 能 ” 的 TestPublisher 。 后 者 需 要 传 入 由
TestPublisher.Violation通过枚举指定的一组选项,这些选项用于告
诉Publisher可以忽略哪些问题。枚举值如下。
◎ REQUEST_OVERFLOW:允许在请求数不足的情况下调用next,而
且不会触发IllegalStateException。
◎ ALLOW_NULL : 允 许 next 发 出 一 个 null 值 而 不 会 触 发
NullPointerException。
◎ CLEANUP_ON_TERMINATE:可以重复多次发出终止信号,包括
complete、error和emit。
最后TestPublisher还可以使用不同的assert*来跟踪其内部的订
阅状态,也可以使用它其中设定转换方法flux和mono将它自己包装为
Flux和Mono来使用。
接下来通过几个Demo来展示上述用法。首先,对正常情况下的
TestPublisher产生null元素下发的空指针异常问题进行测试:

测试通过,由前面next的相关源码可知,起作用的是:

在正常的情况下,并没有设定Violation.ALLOW_NULL,即条件成
立,空指针异常包含的信息与我们测试的期望值相符。于是,使用
TestPublisher.createNoncompliant 来 创 建 一 个 规 避 空 指 针 异 常 的
TestPublisher对象,测试Demo如下:
测试通过。同样,我们来测试请求数充足时的状况:

在这里,通过StepVerifier.create方法得到了一个请求数为1的
测试实例,然后在调用publisher.emit("bar")时就会产生异常,我
们知道,下发元素数量的判定应该在Subscription的下发操作中,这
指 的 就 是 包 装 类 DefaultTestPublisher.TestPublisherSubscription
的onNext方法:
可以看到,首先对元素请求数量进行判断,当元素请求剩余数量
为0且并不存在Violation.REQUEST_OVERFLOW这个条件设定的时候,就
移除这个订阅者,同时会给我们真正的订阅者下发一个onError事件
(actual.onError),且onError事件应该包含的异常信息与我们测试
的预期相同。
注 意 : 在 这 里 , 当 元 素 请 求 数 量 变 为 0 时 ,
parent.hasOverflown=true , 那 么 该 参 数 可 以 用 于
assertRequestOverflow与assertNoRequestOverflow的判断。
同样,当我们希望忽略这个异常的时候,可以使用
TestPublisher.createNoncompliant来进行配置:

测试通过。可能有读者会产生疑问:既然可以进入元素下发阶
段,那么不是应该有一个expectNext("bar")的期望元素吗?在这
里,通过misbehavingAllowsOverflow()这个Demo,我们将对产生的
Signal事件的测试流程进行一下解读。
结合前面所讲,emit会在下发完元素之后执行complete,此时会
调用订阅者DefaultVerifySubscriber的onComplete方法,在这个方法
中会调用onExpectation(Signal.complete()),如下面的源码所
示:
在 这 里 , onExpectation ( Signal.complete ( ) ) 方 法 用 于 对
Signal.complete事件进行测试,在其中会对事件类型进行判断,以选
择对应的处理策略,如下面的源码所示:
此时,就要从我们编制的测试列表中获取下一项测试内容(只获
取,并不移除):this.script.peek,看看它是什么类型的事件。根
据我们在misbehavingAllowsOverflow这个Demo中的设定,下一个事件
类型为SignalEvent,这时就会执行onSignal(actualSignal)方法,
将通过Signal.complete得到的事件作为参数传入其中,如下面的源码
所示:

此时,就要从我们编制的测试列表中获取下一项测试内容(获取
并 移 除 ) : this.script.poll , 并 对 当 前 的 Signal 进 行 测 试 :
signalEvent.test ( actualSignal ) 。 我 们 将
misbehavingAllowsOverflow 这 个 Demo 中 <1> 处 的 注 释 打 开
( expectNext ( "bar" ) ) , 由 于 我 们 传 入 的 Signal 类 型 为
SignalType.ON_COMPLETE , 而 测 试 、 处 理 的 Signal 类 型 为
SignalType.ON_NEXT,两者是不同的,因此会产生异常,此处可以查
看在调用expectNext("bar")时,它内部所涉及的Signal事件处理逻
辑:

结合上面的源码,当我们将misbehavingAllowsOverflow这个Demo
中<1>处的注释打开且重新执行Demo时,就会抛出如下异常:

在 DefaultTestPublisher 调 用 next 方 法 的 时 候 , 作 为 订 阅 者 的
DefaultVerifySubscriber同样会对元素请求数量进行判断:
在 DefaultVerifySubscriber # onNext 内 通 过
checkRequestOverflow(signal)对元素请求数量进行检查。如下面
的 源 码 所 示 , 若 checkRequestOverflow 传 入 的 Signal 类 型 为
SignalType.ON_NEXT , 或 者 元 素 请 求 剩 余 数 量 r 不 小 于 0 且 不 等 于
Long.MAX_VALUE,同时r小于元素生产数量produced(即生产的元素数
量大于请求所需的元素数量限制),那就说明元素下发数量大于元素
请求数量而产生了溢出异常。
最后,在最新的开发分支中(本书基于Reactor 3.1.x),官方已
经加入了针对冷数据源的ColdTestPublisher实现,同样相关的源码可
能有变化,但应该只是做了小优化,此处就不再赘述了,大家可以根
据DefaultTestPublisher和之前所讲内容自行探索。

9.7 使用PublisherProbe检查执行路径
当构建复杂的操作链时,可能会有多个子源序列,从而导致有多
个执行路径。在大多数时候,这些子源序列会产生一个足够明确的
onNext信号,我们可以通过检查最终结果来判断该子源序列是否产生
了订阅。
下面来思考一个场景。首先构建一条操作链,并使用
switchIfEmpty方法在源为空的情况下切换到另一个源,使其与同一个
订阅者产生订阅关系,此订阅关系在switchIfEmpty中间操作所包装订
阅者的onComplete方法内产生:
即只有在左侧源为空的情况下,才会切换到右侧备用源(这是由
once来进行管理的,其初始值为false,一旦左侧源有元素可以下发,
结束时就不会切换到右侧备用源),知道了这些,下面来看一个
Demo,根据具体场景来定义如下方法:
首先,给出测试Demo:

因为有Mono<String> source与Publisher<String> fallback产生


的两条订阅线,所以这里一个测试用例是测不完全的,于是使用了两
个测试Demo,执行后得到了预期的结果。但如果Demo中自定义的方法
返回的是一个Mono<Void>呢?我们来看如下Demo:
在<1>处,then会忽略所有元素,只保留完成信号,所以返回值为
Mono<Void>。在<2>处,通过processOrFallback传入的参数类型可以
知道,doWhenEmpty也是一个Mono<Void>。若在这里还是按照之前使用
的两个测试用例来进行测试,我们就分不清到底执行的是
commandSource还是doWhenEmpty了。
为了验证执行路径是否经过doWhenEmpty,需要写一些额外的代
码,比如需要一个这样的Mono<Void>:
◎ 能够捕获该Mono<Void>被订阅的事实。
◎ 以上事实需要在整个执行过程结束之后再进行验证。
在 Reactor 3.1 之 前 的 版 本 中 , 你 需 要 为 每 一 种 状 态 维 护 一 个
AtomicBoolean变量,然后在相应的doOn*方法中观察它们的值。这样
就需要添加不少额外代码:
这好像有些复杂,有没有更简单的方法?答案是:有。在Reactor
3.1.0之后的版本中,可以使用PublisherProbe来实现以上效果,相关
源码如下:

◎ 在<1>处,创建了一个探针(Probe),它会转化为一个空序列
源。
◎ 在<2>处,需要使用probe.mono来替换我们在上一个Demo中设
定的Mono<Void>类型的对象testFallback。
◎ 在<3>处,序列结束之后,你可以用探针来判断序列是否已使
用,你可以检查它是从哪(条路径)被订阅的。
◎ 在<4>处,对于请求也是一样的。
◎ 在 <5> 处 , 可 以 判 断 执 行 过 程 是 否 被 取 消 了 ( 即 调 用 了
Subscription#cancel方法)。
在使用Flux<T>时,也可以通过调用PublisherProbe#flux方法来
放置探针。如果你既需要用探针检查执行路径,也需要它能够发出数
据,那么你可以通过PublisherProbe.of(Publisher)方法包装一个
Publisher<T>来实现。
其实,DefaultPublisherProbe的内部实现依然是对我们Demo中的
那些原子类变量的封装:
在这里,可以看到AtomicLongArray的实际应用,下面对它进行总
结。AtomicLongArray内部维护的是索引对应的原子类变量,所以这里
定义了一个包含3个元素的AtomicLongArray,同时设定3个final常量
SUBSCRIBED 、 CANCELLED 、 REQUESTED 来 对 应 AtomicLongArray 游 标 位
置。接着可以根据这几个常量对应的数组位置所在的原子类变量值来
判断相应方法有没有被调用过。也就是说,可以在自己的代码中使用
AtomicLongArray来判断多种联合状态((get(A)>0)&&(get(B)
>0)&&(get(C)>0))或执行几个关联状态之间的运算(get(A)
&get(B),比如通过服务链上的相关服务器状态计算出此服务链上的
服务权重质量,然后根据权重质量来做服务器服务优化)。对于其他
几种AtomicXXXArray,也可按照类似思路拓展。

9.8 小结
至此,关于Reactor中的测试已经讲解完毕,希望读者通过对原理
的学习,可以更好地驾驭测试的节奏,然后根据项目的不同情况来合
理地选择测试API。同样,根据相应的实现原理,希望大家可以学到更
多实用的设计理念和具体用法。
第10章 Reactor中的调试
由于响应式编程与传统编程存在差异,因此使用Reactor编写的代
码在出现问题时比较难进行调试。为了更好地帮助开发人员进行调
试,Reactor提供了相应的辅助功能。

10.1 启用调试模式
当需要获取更多与源下发过程相关的执行信息时,可以在程序开
始的地方添加Hook.onOperator等来启用调试模式。在启用调试模式之
后,在执行所有的操作时,都会保存额外的与操作链相关的信息。当
出现错误时,这些信息会作为堆栈异常信息的一部分输出。通过这些
信息可以分析出具体是在执行哪个操作时出现了问题。
当调用Hooks.onOperatorDebug时,其实就是定义了一个回调操
作,然后将其加入onEachOperatorHooks中,接着更新调用链,通过
createOrUpdateOpHook 中 的 实 现 并 结 合 之 前 讲 过 的 以 JDK 中
java.util.function.Function接口为核心的函数过滤链设计(即9.5
节最后涉及的andThen的用法),最后得到操作链。相关源码如下:
观察上面源码中的OnOperatorDebug,这是一个Function对象,其
接 收 一 个 Publisher 对 象 并 返 回 一 个 Publisher 对 象 , 其 内 部 的
apply(Publisher<T> publisher)方法通过识别源的类型来进行相应
的包装(XxxOnAssembly对象,将它看作一个中间操作所代表的包装类
即可,针对的是onEachOperatorHooks)。
在这里,OnOperatorDebug根据源的Mono或Flux类型最终得到的都
是由MonoOnAssembly或FluxOnAssembly包装的实现,而其内部主要设
定了一个Throwable fail(Throwable t)。在产生异常的时候,使用
这个fail方法对异常进行包装,这样就可以得到具体位置的异常信息
(此处源码逻辑比较简单,请读者自行根据报错信息对照查看):
在产生订阅关系的时候也有此包装,只不过其针对的是
onLastOperatorHooks。onOperatorDebug对Flux或Mono进行包装所用
的是onEachOperatorHooks,LastOperatorHook针对的是产生订阅关系
时才会调用的函数,而onEachOperatorHooks则更注重对操作上的功能
进行增强包装。
例如,我们想要添加一个全局的自定义操作,并希望在每一个订
阅关系中都自动回调该操作,或者添加一个用于全局的调试行为,那
么 就 可 以 对 Hooks.onEachOperatorHook 进 行 修 改 , 接 着 调 用 Flux #
onAssembly,然后就可以达到我们的目的,即通过这种方式来包装自
定义的API。
我们更多的还是将Hooks.onEachOperatorHook用于实现全局性质
的 代 码 调 试 , 具 体 方 法 是 , 在 一 开 始 就 定 义
Hooks.onEachOperatorHook,如在flatMap、map等操作前进行定义,
这样会对所定义的操作进行二次包装,如果产生异常,会调用
onAssembly 操 作 对 所 包 装 的 操 作 类 ( 如 onAssembly ( new
FluxMap<>(this,mapper)))产生的异常进行包装并下发(在这
里,读者若细心观察,可以发现通用的中间操作API最后都会返回
onAssembly(...)的包装,这也告诉我们当有类似于官方中间操作
API的扩展需求时,可以通过这种方式对返回方法进行包装):
需要记住的是,千万要在测试源实例化之前定义
Hooks.onOperatorDebug,否则调试模式在产生订阅时不会生效。

10.2 在调试模式下读取堆栈跟踪信息
接下来,通过一个Demo来继续探讨10.1节的内容,并对其堆栈信
息涉及的源码细节进行详细解读,然后通过一两个特殊的Demo来展示
调试模式针对某些情况提供的更加实用的功能。下面先看一个简单的
Demo:
在这里,主动抛出了一个RuntimeException异常,然后捕获打
印 , 通 过 Assert 断 言 来 发 现 异 常 产 生 的 位 置 。 我 们 在
FluxOnAssembly.OnAssemblySubscriber#fail方法(之前提到过)中
对此异常进行分解包装,将自定义的信息(比如产生异常的操作的类
信息)放入Throwable的Suppressed中,以此来提供更详细的异常信
息,帮助我们快速定位异常所在。具体源码如下:
在上面源码的最后,可以看到%%%%%%%%%%%%%%%%%%%%%%%%输出后
没 有 其 他 信 息 输 出 , 所 以 可 以 判 定
Assert.assertTrue ( e.getSuppressed ( )
[0].getMessage().contains("MonoCallable"))成立。
为了更好地查看细节,将e.printStackTrace的内容打印出来,在
<1>处,是正常的异常输出,而在Suppressed中有内容的时候,在异常
输出日志中会将其打印出来,如<2>处所示。在这里,我们来看看
e.printStackTrace与Suppressed是如何结合的,又如何做到随心意输
出的。
那么,再次看看OnAssemblySubscriber#fail的源码细节:
开始时,snapshotStack只是一个新建的默认对象,构造器内也没
有执行自定义的设置操作。在第一次产生异常的时候,通过
OnAssemblySubscriber#fail方法可知,首先调用了FluxOnAssembly
# fillStacktraceHeader , 如 下 面 的 源 码 所 示 , 默 认 的
AssemblySnapshotException#isLight方法返回了false,而且只有此
异常派生子类AssemblyLightSnapshotException的isLight方法才会返
回true,所以if(ase.isLight())不成立。接下来就能看到Demo中
产生的异常日志(<3>处)的信息来源了,snapshotStack是用来对异
常进行信息抽取包装的,初始时其为空,所以其getMessage方法会得
到一个null,于是if(ase.getMessage()!=null)不成立,也就是
异常日志Suppressed的开头并不会出现“described as [”。
然后回到OnAssemblySubscriber#fail方法,在第一次产生异常
的 情 况 下 , if ( set==null ) 条 件 成 立 , 将 suppressed 通 过
addSuppressed 方 法 与 原 生 的 异 常 关 联 起 来 , 此 方 法 需 要 接 收 一 个
Throwable类型的对象,所以就有了OnAssemblyException的包装。这
也是出于对JDK中的printStackTrace(System.err)参数的考虑,后
面会进行具体讲解。
那么通过OnAssemblyException的构造函数可以知道,chainOrder
添加了相关内容,我们关心的主要是snapshotStack.toString的相关
信息。使用snapshotStack.getStackTrace方法(如下面的源码所示)
得到了一个栈跟踪的数组,数组里面的每个元素都代表一个栈帧(栈
中保存了调用一个函数所需的维护信息),而这个数组内的第一个元
素就代表这个栈的顶部,即这个执行序列中的最后一个方法,而数组
内的最后一个元素是这个执行序列中的第一个方法。在前面介绍
OnAssembly等操作包装时说过,其贯穿了从开始到结尾的各个中间操
作,所以只要中间操作有异常,我们就能找到订阅下发过程中该正被
消费元素已执行过的整个中间操作方法序列(请观察下面这一段源码
片段)。
同时,通过chainOrder.add中的extract(message,true)对信
息进行加工,对于加工后数据的打印往往要涉及toString方法,注意
日 志 打 印 出 的 是 OnAssemblyException 的 内 容 , 其 属 于
original.addSuppressed(suppressed)中的suppressed。结合相关
源 码 , 就 可 以 清 楚 地 知 道 Error has been observed by the
following operator(s):的来源了(本节最后会进行进一步的梳
理)。
最后,通过java.lang.Throwable#printStackTrace来再次对照
Demo给出的异常信息的打印顺序,首先打印当前的异常类。在这里,
结 合 前 面 的 java.lang.Throwable # toString 源 码 可 知 ,
OnAssemblyException 作 为 一 个 RuntimeException , 其 message 不 为
null,而为OnAssemblyException#getMessage中的内容。
我们要注意,在testTrace这个Demo中产生订阅关系使用的是Mono
#block操作(如下面的源码所示),BlockingMonoSubscriber在获得
上游源下发的异常后,在BlockingSingleSubscriber#blockingGet方
法中又一次地对其进行了封装。结合Exceptions#propagate可知,得
到 的 是 RuntimeException 类 型 的 异 常 , 那 么 此 时 根 据
java.lang.Throwable # toString 源 码 , 调 用 的 是
java.lang.Throwable 内 的 默 认 实 现 。 我 们 并 不 会 得 到
RuntimeException 派 生 子 类 的 信 息 , 所 以 由 java.lang.Throwable 的
getLocalizedMessage实现可知,得到的message为null。所以,异常
日志首先打印的是java.lang.RuntimeException(在这里,注意将产
生的异常original与我们包装的suppressed区分开)。
接着将整个调用链由最初到现在一步步打印出来,即日志中的一
堆“at”。然后,针对存放的suppressed进行遍历输出,即在其中打
印对象的时候会调用toString方法。最后,若有ourCause,则输出。
相关源码如下:
至此,关于日志已经讲解完毕,有些地方不容易理解,希望大家
多阅读几遍源码。在最新版本的Reactor中,部分代码设计可能有变,
大家要注意进行对比,但代码的核心逻辑不会发生变化。
最后,通过一个Demo来展示关于调试模式的特殊应用。首先,自
定义一个全局操作,并将其作为操作链中的最后一个中间操作执行:
在 本 节 中 , 我 们 依 然 未 涉 及 一 些 内 容 , 例 如
FluxOnAssembly.OnAssemblySubscriber#fail方法在set不为null的
情况下,要对snapshotStack.checkpointed进行判断,这到底是为什
么?同样,在最后的Demo中,可以看到使用了log操作,限于篇幅,也
没有进行深入讲解。其中涉及的思路请大家自行探索,相信通过上面
的学习,读者掌握了一定的源码分析思路。

10.3 通过checkpoint方式进行调试
10.2节提到的调试模式属于全局性的,会影响程序中的每一个订
阅关系,Hooks.onEachOperator更是会影响Flux或Mono所定义的每一
个操作。这种全局性的调试成本较高,会对性能产生较大的影响。也
就是说,只有在我们不确定哪里出问题的时候,才可以使用这种全局
性的调试。
而如果我们能大概定位疑似有问题的具体操作,就可以不用花那
么高的性能成本,可以通过另一种做法来提高性能,降低调试成本,
即通过checkpoint方式来对特定的操作链启用调试模式。在下面的
Demo中,在map操作之后添加了一个名为test的检查点。当出问题时,
检查点名称会出现在异常堆栈信息中。对于程序中重要或者复杂的操
作链,可以在关键位置启用检查点来帮助定位可能存在的问题。

10.4 记录订阅关系下与操作流程相关的日志
开发和调试中的另一项实用功能是,可以将订阅关系下与操作流
程相关的事件记录到日志中。这可以通过添加log操作来实现:

执行结果如下:
在这里,执行了log操作并指定了日志分类的名称为“Test”,这
样就可以根据对应的格式(时间、线程、级别、消息)来输出日志
了。

10.5 小结
本章通过对源码细节的解读和具体实例,展示了与调试相关的源
码和具体应用。同样,对Reactor的整体解读也到此结束。在本系列另
一 本 书 中 , 我 们 会 对 Reactor-Netty 进 行 深 入 解 读 , 并 以 此 来 展 示
Netty 和 Reactor 是 如 何 一 步 步 融 合 的 , 同 时 会 告 诉 大 家 Spring
WebFlux是如何在Reactor-Netty的基础上一步步设计实现的。通过学
习这些内容,大家就可以彻底掌握一整套响应式底层实现方法了。

You might also like