理解Reactive
相关技术
- 反应堆模式(Reactor)
同步非阻塞,多工模式,一个事情可以分为几个步骤,每个步骤相应去做,同步串行先做A,后做B
- Proactor模式
异步非阻塞,多工模式,A,B,C同时去做,异步去做。
- 观察者模式(Observer)
事件通知和监听的模式,也是一种推模式,由服务端推送到客户端。
- 迭代器模式(Iterator)
拉模式,服务端准备好数据,由客户端通过循环去获取。
- Java并发模型
WebFlux的底层核心技术是Reactive,Reactive就是关于同步、异步、多工、或者设计模式的综合体。
关于Reactive的一些讲法
- Reactive是异步非阻塞编程
- Reactive能够提升程序性能
- Reactive能解决传统编程模型遇到的困境
Reactive实现框架
- RxJava:Reactive Extensions Java
在Reactive基础上做了一些扩展,不是WebFlux的底层实现
- Reactor:Spring WebFlux Reactive类库,标准的Spring家族的实现
- Flow API:Java9 Flow API实现,标准的实现了Reactive Stream,有一些并发的扩展
传统编程模型中的某些困境
Reactor认为阻塞可能是浪费的
https://projectreactor.io/docs/core/release/reference/#_blocking_can_be_wasteful
现代应用程序有着大量并发用户,而且,即使现代硬件的能力不断提高,现代软件的性能仍然是一个关键问题。
从广义上讲,有两种方法可以提高程序的性能:
并行化以使用更多线程和更多硬件资源;
在使用现有资源方面寻求更高的效率;
通常,Java开发人员使用阻塞代码编写程序。在出现性能瓶颈之前,这种做法是可行的。然后是时候引入额外的线程,运行类似的阻塞代码了。但是这种资源利用率的扩展会很快引入争用和并发问题。
更糟糕的是,阻塞会浪费资源。如果仔细观察,只要程序涉及一些延迟(特别是I/O,比如数据库请求或网络调用),资源就会被浪费,因为线程(可能有很多线程)现在处于空闲状态,等待数据。
所以并行化方法并非银弹。
- 阻塞导致性能瓶颈和浪费资源
- 增加线程可能会引起资源竞争(锁带来的性能问题,并行变串行)和并发问题(数据同步问题,读写线程,可见性问题)
- 并行的方式不是银弹(不能解决所有问题,线程数和切换成本)
理解阻塞的弊端
阻塞场景 - 数据顺序加载
public class DataLoader { // 模板方法 public final void load() { long startTime = System.currentTimeMillis(); doLoad(); long costTime = System.currentTimeMillis() - startTime; System.out.println("load()总耗时:" + costTime + "毫秒"); } protected void doLoad() { loadConfigurations(); loadUsers(); loadOrders(); } protected final void loadConfigurations() { loadMock("loadConfigurations()", 1); } protected final void loadUsers() { loadMock("loadUsers", 2); } protected final void loadOrders() { loadMock("loadOrders()", 3); } private void loadMock(String source, int seconds) { try { long startTime = System.currentTimeMillis(); long milliseconds = TimeUnit.SECONDS.toMillis(seconds); Thread.sleep(milliseconds); long costTime = System.currentTimeMillis() - startTime; System.out.printf("[线程: %s] %s 耗时: %d 毫秒\n", Thread.currentThread().getName(), source, costTime); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { new DataLoader().load(); } }
运行结果:
[线程: main] loadConfigurations() 耗时: 1001 毫秒
[线程: main] loadUsers 耗时: 2001 毫秒
[线程: main] loadOrders() 耗时: 3000 毫秒
load()总耗时:6035毫秒
结论:
由于加载过程串行执行的关系,导致消耗实现线性累加。Blocking 模式即串行执行 。
理解并行的复杂
并行场景-并行数据加载
public class ParallelDataLoader extends DataLoader { protected void doLoad() { ExecutorService executorService = Executors.newFixedThreadPool(3); //CompletionService是一个接口,ExecutorCompletionService为其实现类 //ExecutorCompletionService在构造函数中会创建一个BlockingQueue // (使用的基于链表的无界队列LinkedBlockingQueue), // 该BlockingQueue的作用是保存Executor执行的结果。 // 当计算完成时,调用FutureTask的done方法。 // 当提交一个任务到ExecutorCompletionService时, // 首先将任务包装成QueueingFuture,它是FutureTask的一个子类, // 然后改写FutureTask的done方法,之后把Executor执行的计算结果放入BlockingQueue中。 CompletionService completionService = new ExecutorCompletionService(executorService); completionService.submit(super::loadConfigurations, null); completionService.submit(super::loadUsers, null); completionService.submit(super::loadOrders, null); int count = 0; while (count < 3) { if (completionService.poll() != null) { count++; } } executorService.shutdown(); } public static void main(String[] args) { new ParallelDataLoader().load(); } }
运行结果:
[线程: pool-1-thread-1] loadConfigurations() 耗时: 1004 毫秒
[线程: pool-1-thread-2] loadUsers 耗时: 2003 毫秒
[线程: pool-1-thread-3] loadOrders() 耗时: 3003 毫秒
load()总耗时:3122毫秒
结论:
明显地,程序改造为并行加载后,性能和资源利用率得到提升,消耗时间取最大者。
Reactor认为异步不一定能够救赎
https://projectreactor.io/docs/core/release/reference/#_asynchronicity_to_the_rescue
寻求更高的效率,可以解决资源浪费的问题,通过编写异步、非阻塞代码,可以让执行切换到另一个使用相同底层资源的活动任务,然后在异步处理完成后返回到当前进程。
但是如何在JVM上生成异步代码? Java提供了两种异步编程模型:
Callbacks:异步方法没有返回值,但是带有一个额外的回调参数(lambda或匿名类),当结果可用时将被调用。一个常见的例子是Swing的EventListener层次结构。
Futures:异步方法立即返回一个Future<T>,异步进程计算一个T值,但是Future对象包装了对它的访问。该值不是立即可用的,可以轮询该对象,直到该值可用为止。例如,运行Callable <T>任务的ExecutorService使用Future对象。
但是两种方法都有局限性。
回调很难组合在一起,会导致难以阅读和维护的代码(称为“回调地狱