最近几年,随着Go、Node 等新语言、新技术的出现,Java 作为服务器端开发语言老大的地位受到了不小的挑战。虽然Java 的市场地位在短时间内并不会发生改变,但Java 社区还是将挑战视为机遇,并努力、不断地提高自身应对高并发服务器端开发场景的能力。
为了应对高并发服务器端开发场景,在2009 年,微软提出了一个更优雅地实现异步编程的方式——Reactive Programming,我们称之为响应式编程。随后,各语言很快跟进,都拥有了属于自己的响应式编程实现。比如,JavaScript 语言就在ES6 中通过Promise 机制引入了类似的异步编程方式。同时,Java 社区也在快速发展,Netflix 和LightBend 公司提供了RxJava 和Akka Stream 等技术,使得Java 平台也有了能够实现响应式编程的框架。
在2017 年9 月28 日,Spring 5 正式发布。Spring 5 发布最大的意义在于,它将响应式编程技术的普及向前推进了一大步。而同时,作为在背后支持Spring 5 响应式编程的框架Spring Reactor,也进入了里程碑式的3.1.0 版本。
响应式编程是一种面向数据流和变化传播的编程范式。 这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。




比较以下的类
CompletableFuture
Stream
Optional
Observable (RxJava 1)
Observable (RxJava 2)
Flowable (RxJava 2)
Flux (Reactor Core)
一、Composable 可组装
Java : 构建流水线,传递单一值,异常对象
RxJava 和 Reactor:链式编程,可以传递N个值
CompletableFuture - 提供很多的.then*()方法,这些方法允许我们构建一个流水线,在不同的执行阶段之间传递一个单一的值(或者没有值),以及传递异常对象。
Stream - 提供很多的可以链式编程方式连接起来的操作,不同的操作阶段之间可以传递N个值。
Optional - 提供一些中间操作,如: .map(), .flatMap(), .filter().
Observable, Flowable, Flux - 跟Stream相同
二、Lazy 延迟执行
Java : 不支持延迟执行
RxJava 和 Reactor:延迟执行,有订阅者的时候才会执行
CompletableFuture - 非延迟执行,它本质上只是一个异步结果的持有者。这些对象创建出来是为了代表对应的工作,CompletableFuture创建的时候,对应的工作已经开始执行了。它不知道关于工作的任何内容,只是关心结果。所以,没有办法能走到上游去从上到下执行整个流水线。当结果被塞到CompletableFuture对象的时候,下一个阶段开始执行。
Stream - 所有的中间操作都是延迟执行的。所有的终端操作,会触发整个计算。
Optional - 非延迟执行,所有的操作会马上发生。
Observable, Flowable, Flux - 延迟执行,没有订阅者的话,什么都不会做,只有当有订阅者的时候才会执行。
三、Reusable 可重用
Java : 支持重用
RxJava 和 Reactor:支持重用
CompletableFuture - 可以重用,它只是在一个值外面做了一层包装。但需要注意一点,这个包装是可更改的。.obtrude*()方法会更改它的内容,如果你确定没有人会调用到这类方法,那么重用它还是安全的。
Stream - 不能重用
Optional - 完全可重用,因为它是不可变对象,而且所有工作都是立即执行的。
Observable, Flowable, Flux - 就是设计来可重用的。所有的执行会从初始点开始,走过所有阶段,前提是有订阅者。
四、Asynchronous(异步)
Java : 支持异步
RxJava 和 Reactor:支持异步
CompletableFuture - 嗯...这个类存在的目的就是异步的把多个操作链接起来。CompletableFuture代表一个工作,后面跟一个Executor关联起来。如果你不明确指定一个executor,那么系统会使用公共的ForkJoinPool线程池来执行。这个线程池可以用ForkJoinPool.commonPool()获取到。默认的设置下它会创建系统硬件支持的线程数一样多的线程(通常就是跟CPU的核心数,如果你的CPU支持超线程,那么可能再翻一倍)。不过你也可以设置ForkJoinPool线程池的线程数,用以下JVM option:
或者每次调用的时候提供一个定制的Executor。
Stream - 不支持创建异步过程,但是可以支持并行的计算——通过stream.parallel()等方式创建并行流。
Optional - 不支持,它只是一个容器。
Observable, Flowable, Flux - 目标就是为了构建异步的系统,但是默认情况下还是同步的。subscribeOn和observeOn允许你来控制消息的订阅以及消息的接收(指定当你的observer的 onNext / onError / onCompleted 被调用的时候做什么事情)。
subscribeOn让你决定用哪个Scheduler来执行Observable.create
五、Cacheable(可缓存)
Java :可缓存
RxJava 和 Reactor:默认不可以缓存,但是调用.cache()后可以
可缓存和可重用之间的区别是什么?举个例子,我们有一个流水线A,并且使用这个流水线两次,创建两个新的流水线 B = A + 以及 C = A + 。
- 如果B和C都能成功完成,那么这个A是可重用的。
- 如果B和C都能成功完成,并且A的每一个阶段只被调用了一次,那么这个A是可缓存的。
可以看出,一个类如果是可缓存的,必然得是可重用的。
CompletableFuture - 跟可重用的答案一样。
Stream - 不能缓存中间操作的结果,除非调用了终端操作。
Optional - ‘可缓存’,实际上,所有工作立即执行,并且做完后就保存了一个不变值,自然‘可缓存’。
Observable, Flowable, Flux - 默认情况下是不可缓存的,但是你可以把一个这些类转变成缓存,只要调用.cache()就可以
六、Push or Pull(推模式还是拉模式)
Java :拉模式 和 推模式
RxJava 和 Reactor:推模式
Stream 和 Optional - 是拉模式的。你调用不同的方法(.get(), .collect() 等)从流水线拉取结果。拉模式经常与阻塞、同步是相关联的,而这也合理。你调用一个方法,然后线程等待数据。线程会阻塞直到数据到达。
CompletableFuture, Observable, Flowable, Flux - 是推模式的。你订阅一个流水线,然后当有东西可以处理的时候你会得到通知。推模式通常意味着非阻塞、异步。当流水线在某个线程上执行的时候,你可以做任何事情。你已经定义了一段待执行的代码,作为下一个阶段的任务,当通知到达的时候,这个代码就会被执行。
七、Backpressure(反压)

Java :不支持背压
RxJava 和 Reactor:支持背压
要做到支持反压,流水线必须是推模式的。
Backpressure(反压) 描述的是在流水线中会发生的一种场景:某些异步的阶段处理速度跟不上,需要告诉上游生产者放慢速度。直接失败是不可接受的,因为会丢失太多数据。

Stream & Optional - 不支持反压,因为他们是拉模式。
CompletableFuture - 不需要面对这个问题,因为它只产生0个或者1个结果。
Observable(RxJava 1), Flowable, Flux - 提供一组方案解决这个问题。常用的策略是:
- Buffering(缓冲) - 把所有的onNext的值保存到缓冲区,直到下游消费它们。
- Drop Recent - 如果下游处理跟不上的话,丢弃最近的onNext值。
- Use Latest - 如果下游处理跟不上的话,只提供最近的onNext值,之前的值会被覆盖。
- None - onNext事件直接被触发,不带任何缓冲或丢弃处理。
- Exception - 如果下游处理跟不上的话,触发一个异常。
Observable(RxJava 2) - 不解决这个问题。很多RxJava 1的使用者用Observable来处理不适用反压的事件,或者是使用Observable的时候不用任何策略处理反压,这会导致不可预知的异常。所以,RxJava 2明确地区分两种情况,提供支持反压的Flowable和不支持反压的Observable。
八、 Operator fusion(操作融合)
Java :不支持
RxJava1 : 不支持
RxJava2 : 支持
Reactor : 支持




上面的内容可以总结为一个表:


继续看一个例子,为什么使用Reactor
例如我们现在要用非阻塞的方式调用一个远程服务,当远程接口数据可用时去做一些业务处理。这时候代码怎么写呢?我们需要提供一个回调函数,然后在响应就绪的时候,去调用我们的回调函数。
传统java 异步回调例子
// 以下案例来自 Reactor 官网
userService.getFavorites(userId, new Callback<List<String>>() {
public void onSuccess(List<String> list) {
if (list.isEmpty()) {
suggestionService.getSuggestions(new Callback<List<Favorite>>() {
public void onSuccess(List<Favorite> list) {
UiUtils.submitOnUiThread(() -> {
list.stream()
.limit(5)
.forEach(uiList::show);
});
}
public void onError(Throwable error) {
UiUtils.errorPopup(error);
}
});
} else {
list.stream()
.limit(5)
.forEach(favId -> favoriteService.getDetails(favId,
new Callback<Favorite>() {
public void onSuccess(Favorite details) {
UiUtils.submitOnUiThread(() -> uiList.show(details));
}
public void onError(Throwable error) {
UiUtils.errorPopup(error);
}
}
));
}
}
public void onError(Throwable error) {
UiUtils.errorPopup(error);
}
});
换成reactor写法后,代码简单多了
// 以下案例来自 Reactor 官网
userService.getFavorites(userId)
.flatMap(favoriteService::getDetails)
.switchIfEmpty(suggestionService.getSuggestions())
.take(5)
.publishOn(UiUtils.uiThreadScheduler())
.subscribe(uiList::show, UiUtils::errorPopup);
再看一个例子
CompletableFuture组合操作的例子
我们首先获取ID的列表,然后我们要通过ID获取一个名称(name)和一个统计信息(Stat),并将它们成对组合,所有这些都是异步的
传统java completableFuture代码
CompletableFuture<List<String>> ids = ifhIds();
CompletableFuture<List<String>> result = ids.thenComposeAsync(l -> {
Stream<CompletableFuture<String>> zip =
l.stream().map(i -> {
CompletableFuture<String> nameTask = ifhName(i);
CompletableFuture<Integer> statTask = ifhStat(i);
return nameTask.thenCombineAsync(statTask, (name, stat) -> "Name " + name + " has stats " + stat);
});
List<CompletableFuture<String>> combinationList = zip.collect(Collectors.toList());
CompletableFuture<String>[] combinationArray = combinationList.toArray(new CompletableFuture[combinationList.size()]);
CompletableFuture<Void> allDone = CompletableFuture.allOf(combinationArray);
return allDone.thenApply(v -> combinationList.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList()));
});
List<String> results = result.join();
assertThat(results).contains(
"Name NameJoe has stats 103",
"Name NameBart has stats 104",
"Name NameHenry has stats 105",
"Name NameNicole has stats 106",
"Name NameABSLAJNFOAJNFOANFANSF has stats 121");
使用Reactor改写
Flux<String> ids = ifhrIds();
Flux<String> combinations =
ids.flatMap(id -> {
Mono<String> nameTask = ifhrName(id);
Mono<Integer> statTask = ifhrStat(id);
return nameTask.zipWith(statTask,
(name, stat) -> "Name " + name + " has stats " + stat);
});
Mono<List<String>> result = combinations.collectList();
List<String> results = result.block();
assertThat(results).containsExactly(
"Name NameJoe has stats 103",
"Name NameBart has stats 104",
"Name NameHenry has stats 105",
"Name NameNicole has stats 106",
"Name NameABSLAJNFOAJNFOANFANSF has stats 121"
);
可以看出Reactor编程可以通过异步化更好的利用当前CPU的资源,更强大的事件编排能力。
1. 为什么是Reactor模式
像netty这样的精品中的极品,肯定也是需要先从设计模式入手的。netty的整体架构,基于了一个著名的模式——Reactor模式。Reactor模式,是高性能网络编程的必知必会模式。
tomcat服务器的早期版本确实是这样实现的。
多线程并发模式,一个连接一个线程的优点是:
一定程度上极大地提高了服务器的吞吐量,因为之前的请求在read阻塞以后,不会影响到后续的请求,因为他们在不同的线程中。这也是为什么通常会讲“一个线程只能对应一个socket”的原因。另外有个问题,如果一个线程中对应多个socket连接不行吗?语法上确实可以,但是实际上没有用,每一个socket都是阻塞的,所以在一个线程里只能处理一个socket,就算accept了多个也没用,前一个socket被阻塞了,后面的是无法被执行到的。
缺点在于资源要求太高,系统中创建线程是需要比较高的系统资源的,如果连接数太高,系统无法承受,而且,线程的反复创建-销毁也需要代价。
改进方法是:
采用基于事件驱动的设计,当有事件触发时,才会调用处理器进行数据处理。使用Reactor模式,对线程的数量进行控制,一个线程处理大量的事件。

顺便说一下,可以将上图的accepter,看做是一种特殊的handler。
4.3. 单线程模式的缺点:
1、 当其中某个 handler 阻塞时, 会导致其他所有的 client 的 handler 都得不到执行, 并且更严重的是, handler 的阻塞也会导致整个服务不能接收新的 client 请求(因为 acceptor 也被阻塞了)。 因为有这么多的缺陷, 因此单线程Reactor 模型用的比较少。这种单线程模型不能充分利用多核资源,所以实际使用的不多。
2、因此,单线程模型仅仅适用于handler 中业务处理组件能快速完成的场景。


7. Reactor编程的优点和缺点
6.1. 优点
1)响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;
2)编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
3)可扩展性,可以方便的通过增加Reactor实例个数来充分利用CPU资源;
4)可复用性,reactor框架本身与具体事件处理逻辑无关,具有很高的复用性;
6.2. 缺点
1)相比传统的简单模型,Reactor增加了一定的复杂性,因而有一定的门槛,并且不易于调试。
2)Reactor模式需要底层的Synchronous Event Demultiplexer支持,比如Java中的Selector支持,操作系统的select系统调用支持,如果要自己实现Synchronous Event Demultiplexer可能不会有那么高效。
3) Reactor模式在IO读写数据时还是在同一个线程中实现的,即使使用多个Reactor机制的情况下,那些共享一个Reactor的Channel如果出现一个长时间的数据读写,会影响这个Reactor中其他Channel的相应时间,比如在大文件传输时,IO操作就会影响其他Client的相应时间,因而对这种操作,使用传统的Thread-Per-Connection或许是一个更好的选择,或则此时使用改进版的Reactor模式如Proactor模式。
reactor 线程模型






主从Reactor-多线程
主从Reactor-多线程相当于将原来的Reactor主线程又分成了两部分,系统的复杂度进一步提升。


Reactor模式具有如下优点
响应快,不必为单个同步事件所阻塞,虽然Reactor本身仍是同步的(因为Reactor下面有多个subReactor,所以当一个subReactor阻塞还可以调用其他的subReactor);
可以最大地避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
扩展性好,可以方便地通过增加Reactor实例个数来充分利用CPU资源;
复用性好,Reactor模型本身与具体时间处理逻辑无关,具有很高的复用性。
















