文章目录
- 同步编程vs异步编程
- 异步编程小故事
- 单JVM
- 异步地处理一些事情,而不需要知道异步任务的结果
- 主线程等待异步任务的执行结果
- Future确实可以获取异步任务的执行结果,但是获取其结果还是会阻塞调用线程的,并没有实现完全异步化处理 --> CompletableFuture
- Reactor、RxJava等反应式API
- RPC框架的异步请求
- 同步RPC调用
- RPC异步调用
- 合并RPC调用结果
- Web
- Servlet的阻塞处理模型
- Servlet 3.0 / 3. 1 非阻塞IO
- WebFlux
- 异步编程框架
- 新兴的语言对异步处理的支持能力
同步编程vs异步编程
同步编程的优点和问题:
- 同步编程, 简单且符合思维习惯,但在性能瓶颈时需要引入更多线程以实现并行化处理。
- 多线程访问共享资源引入了资源争用和并发问题。
- 操作系统限制了线程数量,无法无限增加线程以提高性能。
- 同步阻塞编程浪费资源,例如在网络IO请求中,线程会阻塞等待响应,浪费了其它可用资源。
异步编程的优点:
- 异步编程允许程序并行运行,将工作单元与主应用程序线程分开独立运行,并在完成后通知主应用程序线程结果或失败原因。
- 异步编程提高应用程序性能和响应能力。
- 通过异步方式发起网络IO请求,调用线程不会同步阻塞,可以在等待响应时执行其他任务,提高线程利用率。
- 异步编程可以提供更好的用户体验,允许用户在请求处理中执行其他操作,而不会冻结应用界面。
异步编程小故事
单JVM
异步地处理一些事情,而不需要知道异步任务的结果
比如在调用线程里面异步打日志,为了不让日志打印阻塞调用线程,会把日志设置为异步方式。如图 所示的日志异步化打印,使用一个内存队列把日志打印异步化,然后使用单一消费线程异步处理内存队列中的日志事件,执行具体的日志落盘操作(本质是一个多生产单消费模型),在这种情况下,调用线程把日志任务放入队列后会继续执行其他操作,而不再关心日志任务具体是什么时候入盘的。
在Java中,每当我们需要执行异步任务时,可以直接开启一个线程来实现,也可以把异步任务封装为任务对象投递到线程池中来执行。
在Spring框架中提供了@Async注解把一个任务异步化来进行处理。
主线程等待异步任务的执行结果
这时候Future就派上用场了。比如调用线程要等任务A执行完毕后再顺序执行任务B,并且把两者的执行结果拼接起来供前端展示使用,如果调用线程是同步调用两次任务 ,则整个过程耗时为执行任务A的耗时加上执行任务B的耗时。
【同步调用】
【异步调用】
如果使用异步编程 ,则可以在调用线程内开启一个异步运行单元来执行任务A,开启异步运行单元后调用线程会马上返回一个Future对象(futureB),然后调用线程本身来执行任务B,等任务B执行完毕后,调用线程可以调用futureB的get()方法获取任务A的执行结果,最后再拼接两者的结果。
这时由于任务A和任务B是并行运行的,所以整个过程耗时为max(调用线程执行任务B的耗时,异步运行单元执行任务A的耗时)。
可见整个过程耗时显著缩短,对于用户来说,页面响应时间缩短,用户体验会更好,其中异步单元的执行一般是由线程池中的线程执行。
Future确实可以获取异步任务的执行结果,但是获取其结果还是会阻塞调用线程的,并没有实现完全异步化处理 --> CompletableFuture
使用Future确实可以获取异步任务的执行结果,但是获取其结果还是会阻塞调用线程的,并没有实现完全异步化处理,所以在JDK8中提供了CompletableFuture来弥补其缺点。CompletableFuture类允许以非阻塞方式和基于通知的方式处理结果,其通过设置回调函数方式,让主线程彻底解放出来,实现了实际意义上的异步处理。
【CompletableFuture异步执行】
Reactor、RxJava等反应式API
JDK8还引入了Stream,旨在有效地处理数据流(包括原始类型),其使用声明式编程让我们可以写出可读性、可维护性很强的代码,并且结合CompletableFuture完美地实现异步编程。
但是它产生的流只能使用一次,并且缺少与时间相关的操作(例如RxJava中基于时间窗口的缓存元素),虽然可以执行并行计算,但无法指定要使用的线程池。
同时,它也没有设计用于处理延迟的操作(例如RxJava中的defer操作),所以Reactor、RxJava等Reactive API就是为了解决这些问题而生的。
Reactor、RxJava等反应式API也提供Java 8 Stream的运算符,但它们更适用于流序列(不仅仅是集合),并允许定义一个转换操作的管道,该管道将应用于通过它的数据(这要归功于方便的流畅API和Lambda表达式的使用)。
Reactive旨在处理同步或异步操作,并允许你对元素进行缓冲(buffer)、合并(merge)、连接(join)等各种转换。
RPC框架的异步请求
上面讲解了单JVM内的异步编程,那么对于跨网络的交互是否也存在异步编程范畴呢?
同步RPC调用
对于网络请求来说,同步调用是比较直截了当的。比如我们在一个线程A中通过RPC请求获取服务B和服务C的数据,然后基于两者的结果做一些事情。在同步调用情况下,线程A需要调用服务B,然后同步等待服务B结果返回后,才可以对服务C发起调用,等服务C结果返回后才可以结合服务B和C的结果执行其他操作。
线程A同步获取服务B的结果后,再同步调用服务C获取结果,可见在同步调用情况下业务执行语义比较清晰,线程A顺序地对多个服务请求进行调用
RPC异步调用
但是同步调用意味着当前发起请求的调用线程在远端机器返回结果前必须阻塞等待,这明显很浪费资源。好的做法应该是在发起请求的调用线程发起请求后,注册一个回调函数,然后马上返回去执行其他操作,当远端把结果返回后再使用IO线程或框架线程池中的线程执行回调函数。
那么如何实现异步调用?在Java中NIO的出现让实现上面的功能变得简单,而高性能异步、基于事件驱动的网络编程框架Netty的出现让我们从编写繁杂的Java NIO程序中解放出来,现在的RPC框架,比如Dubbo底层网络通信,就是基于Netty实现的。Netty框架将网络编程逻辑与业务逻辑处理分离开来,在内部帮我们自动处理好网络与异步处理逻辑,让我们专心写自己的业务处理逻辑,而Netty的异步非阻塞能力与CompletableFuture结合则可以轻松地实现网络请求的异步调用。
在执行RPC(远程过程调用)调用时,使用异步编程可以提高系统的性能。如所示,在异步调用情况下,当线程A调用服务B后,会马上返回一个异步的futureB对象,然后线程A可以在futureB上设置一个回调函数;接着线程A可以继续访问服务C,也会马上返回一个futureC对象,然后线程A可以在futureC上设置一个回调函数。
在异步调用情况下,线程A可以并发地调用服务B和服务C,而不再是顺序的。由于服务B和服务C是并发运行,所以相比同步调用,线程A获取到服务B和服务C结果的时间会缩短很多(同步调用情况下的耗时为服务B和服务C返回结果耗时的和,异步调用情况下耗时为max(服务B耗时,服务C耗时))。
合并RPC调用结果
这里可以借助CompletableFuture的能力等两次RPC调用都异步返回结果后再执行其他操作,这时候调用流程如下图所示。
如图所示,调用线程A首先发起服务B的远程调用,会马上返回一个futureB对象,然后发起服务C的远程调用,也会马上返回一个futureC对象,最后调用线程A使用代码futureB.thenCombine(futureC,action)
等futureB和futureC结果可用时执行回调函数action。这里我们只是简单概述下基于Netty的异步非阻塞能力以及Completable-Future的可编排能力,基于这些能力,我们可以实现功能很强大的异步编程能力。
其实,有了CompletableFuture实现异步编程,我们可以很自然地使用适配器来实现Reactive风格的编程。当我们使用RxJava API时,只需要使用Flowable的一些函数转换CompletableFuture为Flowable对象即可 。
Web
上面讲解了网络请求中RPC框架的异步请求,其实还有一类,也就是Web请求
Servlet的阻塞处理模型
,在Web应用中Servlet占有一席之地。在Servlet3.0规范前,Servlet容器对Servlet的处理都是每个请求对应一个线程这种1:1的模式进行处理的 ,每当收到一个请求,都会开启一个Servlet容器内的线程来进行处理,如果Servlet内处理比较耗时,则会把Servlet容器内线程使用耗尽,然后容器就不能再处理新的请求了。
Servlet 3.0 / 3. 1 非阻塞IO
Servlet 3.0规范中则提供了异步处理的能力,让Servlet容器中的线程可以及时释放,具体Servlet业务处理逻辑是在业务自己的线程池内来处理;
虽然Servlet 3.0规范让Servlet的执行变为了异步,但是其IO还是阻塞式的。IO阻塞是说在Servlet处理请求时,从ServletInputStream中读取请求体时是阻塞的,而我们想要的是当数据就绪时直接通知我们去读取就可以了,因为这可以避免占用我们自己的线程来进行阻塞读取,好在Servlet 3.1规范提供了非阻塞IO来解决这个问题.
WebFlux
虽然Servlet技术栈的不断发展实现了异步处理与非阻塞IO,但是其异步是不彻底的,因为受制于Servlet规范本身,比如其规范是同步的(Filter,Servlet)或阻塞的(getParameter,getPart)。
所以新的使用少量线程和较少的硬件资源来处理并发的非阻塞Web技术栈应运而生——WebFlux,其是与Servlet技术栈并行存在的一种新技术,基于JDK8函数式编程与Netty实现天然的异步、非阻塞处理
异步编程框架
为了更好地处理异步编程,降低异步编程的成本,一些框架也应运而生,
比如高性能线程间消息传递库Disruptor,其通过为事件(event)预先分配内存、无锁CAS算法、缓冲行填充、两阶段协议提交来实现多线程并发地处理不同的元素,从而实现高性能的异步处理。
比如Akka基于Actor模式实现了天然支持分布式的使用消息进行异步处理的服务;比如高性能分布式消息中间件Apache RocketMetaQ实现了应用间的异步解耦、流量削峰。
新兴的语言对异步处理的支持能力
Go语言就是其中之一,其通过语言层面内置的goroutine与channel可以轻松实现复杂的异步处理能力。