认识Java异步编程

一 、认识异步编程

一般Java开发人员喜欢使用同步代码编写程序,由于这种请求(request)/响应(response)的方式比较简单,而且比较符合编程人员的思惟习惯;这种作法很好,直到系统出现性能瓶颈;在同步编程方式时因为每一个线程同时只能发起一个请求并同步等待返回,因此为了提升系统性能,此时咱们就须要引入更多的线程来实现并行化处理;可是多线程下对共享资源进行访问时,不可避免会引入资源争用和并发问题;另外操做系统层面对线程的个数是有限制的,不可能经过无限的增长线程数来提供系统性能;最后使用同步阻塞的编程方式还会致使浪费资源,好比发起网络IO请求时候,调用线程就会处于同步阻塞等待响应结果的状态,而这时候调用线程明明能够去作其余事情,等网络IO响应结果返回后在对结果进行处理。html

可见经过增长单机系统线程个数的并行编程方式并非灵丹妙药;经过编写异步、非阻塞的代码,则可使用相同的底层资源将执行切换到另外一个活动任务,而后在异步处理完成后在返回到当前线程进行继续处理,从而提升系统性能。前端

异步编程是可让程序并行运行的一种手段,其可让程序中的一个工做单元与主应用程序线程分开独立运行,而且等工做单元运行结束后通知主应用程序线程它的运行结果或者失败缘由。使用它有许多好处,例如能够提升应用程序的性能和响应能力。算法

好比当调用线程使用异步方式发起网络IO请求后,调用线程就不会同步阻塞等待响应结果,而是在内存保存请求上下文后,会立刻返回后作其余事情,等网络IO响应结果返回后在使用IO线程通知业务线程响应结果已经返回,而后业务线程在对结果进行处理。可知异步调用方式提升了线程的利用率,让系统有更多的线程资源来处理更多的请求。编程

好比在移动应用程序中,在用户操做移动设备屏幕发起请求后,若是是同步等待后台服务器返回结果,则当后台服务操做很是耗时时,就会形成用户看到移动设备屏幕冻结(一直处理请求处理中),在结果返回前,用户不能操做移动设备的其余功能,这对用户体验很是很差。而使用异步编程则当发起请求后,调用线程会立刻返回,具体返回结果则会经过UI线程异步进行渲染,而在这期间用户可使用移动设备的其余功能。缓存

2、 异步编程场景概述

在平常开发中咱们常常会遇到这样的状况,就是须要异步的处理一些事情,而不须要知道异步任务的结果;好比在调用线程里面异步打日志,为了避免让日志打印阻塞调用线程,会把日志设置为异步方式。以下图1-2-1日志异步化打印,使用一个内存队列把日志打印异步化,使用单一线程来消费队列里面日志事件执行具体的日志落盘操做(本质是一个多生产单消费模型),这种状况下调用线程把日志任务放入队列后就继续去干本身的事情了,而再也不关心日志任务具体是何时入盘的;服务器

图 1-2-1 日志异步打印

在Java中每当咱们须要执行异步任务的时候咱们能够直接开启一个线程来实现,也能够把异步任务封装为任务对象投递到线程池里面来执行,在Spring框架中则提供了@Async注解把一个任务异步化来进行处理,这些内容会在后面章节具体讲解。网络

另外有时候咱们还须要在主线程等待异步任务的执行结果,这时候Future就排上用场了;好比调用线程要等执行任务A执行完毕后在顺序执行任务B,而且把二者结果拼接起来做为前端展现使用,若是调用线程是同步调用两次查询(以下图1-2-2同步调用),则整个过程耗时时间为执行任务A的耗时加上执行任务B的耗时。多线程

图1-2-2 同步调用

若是使用异步编程(以下图1-2-3)则能够在调用线程内开启一个异步运行单元来执行任务A,开启异步运行单元后调用线程会立刻返回一个Future对象(futureB),而后调用线程自己来执行任务B,等任务B执行完毕后,调用线程能够调用futureB的get()方法获取任务A的执行结果,最后在拼接二者结果。这时因为任务A和任务B是并行运行的,因此整个过程耗时为max(调用线程执行任务B耗时,异步运行单元执行任务A耗时)。并发

图1-2-3 异步调用

可见整个过程耗时有显著缩短,对于用户来讲页面响应时间会更短,对用户体验会更好,其中异步单元的执行通常是线程池中的线程。框架

使用Future确实能够获取异步任务的执行结果,可是获取其结果仍是会阻塞调用线程的,并无实现彻底异步化处理,在JDK8中提供了CompletableFuture来弥补了其缺点。CompletableFuture类容许以非阻塞方式和基于通知的方式处理结果,其经过设置回调函数方式,让主线程完全解放出来,作本身的事情,实现了实际意义上的异步处理;

以下图1-2-4使用CompletableFuture时候当异步单元返回futureB后,调用线程能够在其上调用whenComplete方法设置一个回调函数action,而后调用线程就会立刻返回了,等异步任务执行完毕后会使用异步线程来执行回调函数action,而无需调用线程干预,若是你对CompletableFuture不了解,不要紧,后面章节咱们会详细讲解,这里你只须要知道其解决了传统Future的缺陷就能够了。

图1-2-4 CompletableFuture异步执行

JDK8还引入了Stream,它旨在有效地处理数据流(包括原始类型),其使用声明式编程让咱们能够写出可读性、可维护性很强的代码,而且结合CompletableFuture能够完美的实现异步编程。可是它产生的流只能使用一次,而且缺乏与时间相关的操做(例如RxJava中的基于时间窗口的缓存元素),虽然能够执行并行计算,但没法指定要使用的线程池。而且它尚未设计用于处理延迟的操做(例如RxJava中的defer操做);而Reactor或RxJava等Reactive API就是为了解决这些问题而生的。

Reactor或RxJava等反应式API也提供Java 8 Stream的运算符,但它们更适用于任何流序列(不只仅是集合),并容许定义一个转换操做的管道,该管道将应用于经过它的数据,这要归功于方便的流畅API和Lambda表达式的使用。Reactive旨在处理同步或异步操做,并容许您缓冲(buffer)、合并(merge)、链接(join) 元素等对元素作各类转换。

上面咱们讲解了单JVM内的异步编程,那么对于跨网络的交互是否也存在异步编程范畴那?对于网络请求来讲,同步调用时比较直截了当的,好比咱们在一个线程A中经过RPC请求获取服务B和服务C的数据,而后基于二者结果作一些事情。在同步调用状况下,线程A须要调用服务B,而后须要同步等待服务B结果返回后,才能够对服务C发起调用,而后等服务C结果返回后才能够结合服务B和C的结果作一件事,以下图1-2-5:

图1-2-5 同步RPC调用

如上图1-2-5线程A同步获取服务B结果后,在同步调用服务C获取结果,可见在同步调用状况下业务执行语义比较清晰,线程A顺序的对多个服务请求进行调用;可是同步调用意味着当前发起请求的调用线程在远端机器返回结果前必须阻塞等待,这明显很浪费资源。好的作法应该是发起请求的调用线程发起请求后,注册一个回调函数,而后立刻返回去作其余事情,当远端把结果返回后在使用IO线程执行回调函数。

那么如何实现异步调用?在Java中NIO的出现让实现上面的功能变得简单,而高性能异步、基于事件驱动的网络编程框架Netty的出现让咱们从编写繁杂的Java NIO程序出解放出来了,如今的RPC框架好比Dubbo底层网络通讯就是基于Netty实现的;Netty框架将网络编程逻辑与业务逻辑处理分离开来,其内部帮咱们自动处理好网络与异步处理逻辑,让咱们专心写本身的业务处理逻辑,Netty的异步非阻塞能力与CompletableFuture结合就能够轻松的实现网络请求的异步调用。

在执行RPC(远程过程调用)调用时候,使用异步编程能够提升系统的性能;以下图1-2-6,在异步调用状况下,当线程A调用服务B后,立刻会返回一个异步的futureB对象,而后线程A能够在futureB上设置一个回调函数;而后线程A能够继续访问服务C,也会立刻返回一个futureC对象,而后线程A能够在futureC上设置一个回调函数:

图1-2-6 RPC异步调用

如上图1-2-6可知异步调用状况下线程A能够并发的调用服务B和服务C,而再也不是顺序的,因为服务B和服务C是并发运行,因此相比线程A同步调用,线程A获取到服务B和服务C结果的时间会缩短不少(同步调用状况下耗时时间为服务B和服务C返回结果耗时的和,异步调用时候耗时为max(服务B耗时,服务C耗时));另外这里能够借助CompletableFuture的能力等两次RPC调用都异步返回结果后作一件事情,这时候调用流程以下图图1-2-7:

图1-2-7 合并Rpc调用结果

如上图图1-2-7调用线程A首先发起服务B的远程调用,而后立刻返回一个futureB对象,而后发起服务C的远程调用,而后立刻返回一个futureC对象,最后调用线程A使用代码futureB.thenCombine(futureC,action)等futureB和futureC结果可用时候执行回调函数action;这里咱们只是简单的概述下基于Netty的异步非阻塞能力以及CompletableFuture的可编排能力,咱们能够实现功能很强大的异步编程能力,后面章节咱们会以Dubbo框架为例讲解其借助Netty的非阻塞异步API实现了服务消费端的异步调用。

其实有了CompletableFuture实现异步编程,咱们能够很天然的使用适配器来实现Reactive风格的编程,当咱们使用RxJava API时候咱们只须要使用Flowable的一些函数转换CompletableFuture为Flowable对象便可,这个咱们在后面章节也会讲述。

上节讲解了网络请求中的RPC框架的异步请求,其实还有一类,也就是Web请求,在Web应用中Servlet占有一席之地。在Servlet3.0规范前,Servlet容器对Servlet的处理都是每一个请求对应一个线程这种1:1的模式进行处理的(以下图1-2-8),每当来一个请求时候都会开启一个Servlet容器内的线程来进行处理,若是Servlet内处理比较耗时,则会把Servlet容器内线程使用耗尽,而后容器就不能再处理新的请求。

图1-2-8 Servlet的阻塞处理模型

Servlet3.0规范中则提供了异步处理的能力,让Servlet容器中的线程能够及时释放,具体Servlet业务处理逻辑是在业务本身线程池内来处理;虽然Servlet3.0规范让Servlet的执行变为了异步,可是其IO仍是阻塞式的,IO阻塞是说在Servlet处理请求时候从ServletInputStream中读取请求体时候是阻塞的,而咱们想要的是当数据已经就绪时候通知咱们去读取就能够了,由于这能够避免占用咱们本身的线程来进行阻塞读取,Servlet3.1规范则提供了非阻塞IO来解决这个问题。

虽然Servlet技术栈的不断发展实现了异步处理与非阻塞IO,可是其异步是不完全的,由于受制于Servlet规范自己,好比其规范是同步的(Filter,Servlet)或阻塞(getParameter,getPart)。因此新的使用少许线程和较少的硬件资源来处理并发的非阻塞Web技术栈应运而生-WebFlux,其是与Servlet技术栈并行存在的一种新的技术,其基于JDK8函数式编程与Netty实现自然的异步、非阻塞处理,这些咱们在后面章节会具体介绍。

另外为了更好的处理异步编程,下降咱们异步编程的成本,一些框架也应运而生,好比高性能线程间消息传递库Disruptor,其经过为事件(events)预先分配内存、无锁CAS算法、缓冲行填充、两阶段协议提交来实现多线程并发的处理不一样的元素,从而实现高性能的异步处理;好比Akka其基于Actor模式实现了自然支持分布式的使用消息进行异步处理的服务;好比高性能分布式消息中间件Apache RocketMetaQ用来实现应用间的异步解耦、流量削峰。

一些新兴的语言对异步处理的支持能力让咱们忍不住称赞,GoLang就是其中之一,其经过语言层面内置的goroutine与channel能够轻松的实现复杂的异步处理能力。

《Java异步编程实战》),一书则是根据上述介绍的次序,把内容划分了若干章节,每章则具体展开讨论相应的异步编程技术。

3、 为什么写做本书

异步编程是可让程序并行运行的一种手段,其可让程序中的一个工做单元与主应用程序线程分开独立运行,使用它有许多好处,例如能够提升应用程序的性能和响应能力。

虽然Java中不一样技术域提供了相应的异步编程技术,可是对异步编程技术的描述散落到了不一样技术域的技术文档中,并无一个统一的地方对这些技术进行梳理概括。另外这些技术之间是什么关系,各自的出现都是为了解决什么问题,咱们也很难找到资料来解释。

本书的出现则是为了打破这种局面,本书旨在把Java中相关的异步编程技术进行概括分类总结,而后呈现给你们,让你们能够有一个统一的地方来查看与探究。

4、本书特点

本书涵盖了Java中常见的异步编程场景,这包含单JVM内的异步编程、以及跨主机经过网络通信的远程过程调用的异步调用与异步处理、以及Web请求的异步处理等等。

本书在讲解Java中每种异步编程技术时都附有案例,以便理论与实践进行结合。

本书在讲解每种异步编程技术时大多都会对其实现原理进行讲解,以便让读者知其然也知其因此然。

本书对最近比较火的反应式编程以及WebFlux的使用与原理解析有必定深刻的探索。

5、 业界推荐

8.png


本文做者:加多

阅读原文

本文为阿里云内容,未经容许不得转载。