Java 异步编程导论

异步编程是可以让程序并行运行的一种手段,其可以让程序中的一个工作单元与主应用程序线程分开独立运行,并且等工作单元运行结束后通知主应用程序线程它的运行结果或者失败原因。使用它有许多好处,例如改进的应用程序性能和减少用户等待时间等。

本文已参加 GitChat「我的技术实践」有奖征文活动,活动链接: GitChat「我的技术实践」有奖征文活动

Java 异步编程导论

异步编程是可以让程序并行运行的一种手段,其可以让程序中的一个工作单元与主应用程序线程分开独立运行,并且等工作单元运行结束后通知主应用程序线程它的运行结果或者失败原因。使用它有许多好处,例如改进的应用程序性能和减少用户等待时间等。

在日常开发中我们经常会遇到这样的情况,就是需要异步的处理一些事情,而主线程不需要知道异步任务的结果,最常见的是在调用线程里面异步打日志,在高并发系统中为了不让日志打印阻塞调用线程,会把日志设置为异步方式,也就是使用一个队列把日志打印异步化,这种情况下调用线程把日志任务放入队列后就继续去干自己的事情了,而不再关心日志任务具体是什么时候入盘的。在 Spring 框架中提供的@Async 注解就可以把一个任务异步化来进行处理,这个后面章节会具体讲解。

另外有时候我们还需要开启异步任务执行后,在主线程等待异步任务的执行结果,这时候 Future 就排上用场了,比如线程 A 要做从数据库 I 和数据库 II 查询一条记录,并且把两者结果拼接起来作为前端展示使用,如线程 A 是同步调用两次查询,则整个过程耗时时间为访问数据库 I 的耗时加上访问数据库 II 的耗时,如下图:

Java 异步编程导论_第1张图片

如果为异步调用则可以在线程 A 内开启一个异步运行单元来从数据库 I 获取数据,然后线程 A 本身来从数据库 II 获取数据,并且等两者结果都返回后,在拼接两者结果,这时候整个过程耗时为 max(线程 A 从数据库 II 获取数据耗时,异步运行单元从数据库 I 获取数据耗时),如下图:

Java 异步编程导论_第2张图片

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

使用 Future 确实可以获取异步任务的执行结果,但是获取其结果还是会阻塞调用线程的,并没有实现完全异步化处理,在 JDK8 中提供了 CompletableFuture 来弥补了其缺点,实现了实际意义上的异步处理。

Java 8 引入了 lambdas 和 CompletableFuture,Lambdas 允许编写简洁的回调,而 CompletionStage 接口和 CompletableFuture 类最终允许以非阻塞方式和基于推送的方式处理结果,其通过设置回调函数方式,让主线程彻底解放出来,做自己的事情。

Java 8 还引入了 Stream,它旨在有效地处理数据流(包括原始类型),这些数据流可以在没有延迟或很少延迟的情况下访问,其使用声明式编程让我们可以写出可读性可维护性很强的代码,其结合 CompletableFuture 可以完美的实现异步编程。但是它是基于拉的,只能使用一次,缺少与时间相关的操作,虽然可以执行并行计算,但无法指定要使用的线程池。它还没有设计用于处理延迟的操作,例如 I / O 操作。这就是 Reactor 或 RxJava 等 Reactive API 的用武之地。

Reactor 或 RxJava 等反应性 API 也提供 Java 8 Stream 等运算符,但它们更适用于任何流序列(不仅仅是集合),并允许定义一个转换操作的管道,该管道将应用于通过它的数据,这要归功于方便的流畅 API 和使用 lambdas。它们旨在处理同步或异步操作,并允许您缓冲,合并,连接或对数据应用各种转换。

另外对于网络传输来说,同步调用时比较直截了当的,但是同步调用意味着当前发起请求的机器中的线程在远端机器返回结果前必须阻塞等待,这明显很浪费资源,好的做法应该是发起请求的机器发起调用线程发起请求后,注册一个回调函数,然后马上返回去做其他事情,当远端把结果返回后在使用 IO 线程或者通过 IO 线程切换到业务线程池执行回调函数,也就是发起方实现了异步调用,调用线程不会被阻塞。

比如在使用 rpc(远程过程调用)发起请时候,使用异步编程也可以提高系统的性能,比如我们在一个线程 A 中通过 rpc 请求获取服务 B 和服务 C 的数据然后基于两者结果做一些事情。在同步 rpc 调用情况下,线程 A 需要调用服务 B 后需要等待服务 B 结果返回后,才可以对服务 C 发起调用,然后等服务 C 结果返回后才可以结合服务 B 和 C 的结果做一件事,如下图:

Java 异步编程导论_第3张图片

线程 A 同步获取服务 B 结果后,在同步调用服务 C 获取结果,可见在同步调用情况下线程 A 必须顺序的对多个服务请求进行调用。

而在异步调用情况下,当线程 A 调用服务 B 时候,服务 B 直接会返回一个异步的 futureB 对象,然后线程 A 可以继续访问服务 C,服务 C 也会返回一个 futureC 对象,然后线程 A 就可以基于 futureB 和 futureC 来获取最终的返回结果,然后基于结果做一些事情,如下图:

Java 异步编程导论_第4张图片

可知异步调用情况下线程 A 可以并发的调用服务 B 和服务 C,而不再是顺序的,由于服务 B 和服务 C 是并发运行,所以相比线程 A 同步调用,线程 A 获取到服务 B 和服务 C 结果的时间会缩短很多(同步调用情况下耗时时间为服务 B 和服务 C 返回结果耗时的和,异步调用时候耗时为 max(服务 B 耗时,服务 C 耗时)),后面章节我们会以 Dubbo 框架为例其借助 Netty 的非阻塞异步 API 实现了服务消费端的异步调用。

在 Web 应用中 Servlet 占有一席之地,在 Servlet3.0 规范前,Servlet 容器对 Servlet 的处理都是每个请求对应一个线程这种 1:1 的模式进行处理的,每当来一个请求时候都会开启一个 Servlet 容器内的线程来进行处理,如果 Servlet 内处理比较耗时,则会把 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 算法、缓冲行填充、两阶段协议提交来实现多线程并发的处理不同的元素,从而实现高性能的异步处理(如果你对这些技术不熟悉的话,可以参考作者的《Java 并发编程之美》一书);比如 Akka 其基于 Actor 模式实现了天然支持分布式的使用消息进行异步处理的服务。

一些新兴的语言对异步处理的支持能力让我们忍不住称赞,golang 就是其中之一,其通过 goroutine 与 channel 可以轻松的实现复杂的异步处理能力。

总结

异步、非阻塞、可编排的编程模型突破了传统编程模型限制,是现在乃至未来编程模型演变的趋势。更多技术分享,请关注微信公众号:技术原始积累。

阅读全文: http://gitbook.cn/gitchat/activity/5d5ff3e84e8c4c49374f23f0

您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。

FtooAtPSkEJwnW-9xkCLqSTRpBKX

你可能感兴趣的:(Java 异步编程导论)