写点什么

性能之争:响应式编程真的有效吗?

  • 2019-04-24
  • 本文字数:3522 字

    阅读完需:约 12 分钟

性能之争:响应式编程真的有效吗?

响应式编程为 Java 的企业版应用提供了更高的性能,并降低了内存消耗,主要是通过减少进程的上下文切换来实现的。因为类似的上下文切换对 CPU 和内存的消耗是极大,所以要尽可能的减少这样的切换操作。不过,响应式编程带来的这种性能上的提高,代价是降低了软件的维护性,这样的代价交换是否值得呢?让我们在本文中深入地讨论一下这个问题吧。


在 Java 的早期时代,抽象线程是区别于其他编程语言的一大优势。直至今日,Java 依然提供了便捷的并行编程和同步的机制。在此基础上,我们可以非常轻松地实现 Web 框架,使 Web 请求可以直接与 servlet API 中的线程绑定在一起,以一种“虚拟”的方式处理请求,摒弃掉同步和并发的问题。调用链在到达 Tabbed Browsing 和 Ajax 之前,还可以设计确定同一 Session 的两个请求不需要并行执行在一个线程,对于普通开发者而言,就不需要担心 Session 级别的并行问题了。


但是目前,Java 的线程在实现上有一个严重的缺陷:Java 线程的实现被操作系统认定为系统进程,这使得线程的切换等价于操作系统的上下文切换,这个代价非常高。如果一个应用程序每分钟内只处理几千个请求,或许没什么问题。可是目前 Web 程序的需求与日俱增,不断增长的用户量和请求应用致使企业应用现在每分钟处理的数据量远远高于 15 年前。由操作系统线程来处理请求的这种模型渐渐地进入了瓶颈,尤其是当你在程序执行的过程中,阻塞住当前程序来访问数据库或者访问其他的微服务,这种情境下特别明显。


并行计算的时间应明显高于 Java 将线程实现为操作系统线程的时间,但是目前请求的执行时间很短,操作系统进程上下文的切换时间很长,完全不成比例。


而这正是响应式编程的用武之地,它与目前的 Java 线程模型完全相反。目前的线程模型是保证所有的事情都在当前线程执行,但是在响应式编程中,异步是一个准则。一个程序执行过程被认定为一系列异步执行的事件,每个事件都被 Publisher 创建,你不需要关心 Publisher 在哪个线程中创建。在响应式程序里面,程序代码包含了监听和执行异步事件的功能,而且会在必要时提供新的事件。


这个方式在某些场景下很有效果,比如说在程序里面访问外部数据库。传统的企业级 Java 程序里面,系统会发送一段 SQL 语句到数据库上面,阻塞住程序,直到数据库返回查询结果。但是在响应式编程里面,程序会跳过等待结果的过程,正常向下执行代码。当你发送一个 SQL 请求到数据库的时候,会用一个 Pushlisher 来替代阻塞的过程,调用者可以注册这个 Pushlisher,这样的话,在之后数据库返回结果的时候,就会通知 Publisher,然后 Publisher 会通知调用者。


这种方式对于解决回调地狱很有帮助,很多异步编程也确实是这样做的。


响应式编程的好处就是执行的代码和执行的线程是分开的。因此在操作系统的层面上,上下文切换的代价比较低。


这种方式在服务端的架构里面有很大的意义,只保留一个线程处理程序进程层面的代码,跟 Node.js 很相似。如果这个线程被阻塞的话,那整个服务器就不会再处理其他请求了。因此,在 JavaScript 中,每个调用在一开始的时候就是异步执行,在任何情况下都可以通过响应式的 API 或者其他有用的抽象方式,来解决回调地狱的问题。


但是正如文章开篇里面提到的,响应式编程也有一些比较严重的问题,写入的代码和执行的代码分离开来,导致阅读和编写代码的难度增加,对于这种异步的代码,编写单元测试和调试代码都很困难。


在与传统的企业应用集成的过程中,也出现了很多新的问题,像安全认证、事务还有链路追踪等仍然附加在当前线程里面。当你开始执行响应式编程的程序时,这种机制也无法奏效,所以需要找到一个更好的解决办法。


Project Reactor 是 Spring 的 Reactive Web Framework 的基础,目前支持测试,调试等等(详情)。不过,也是由于响应式编程的复杂度很高,所以开发者开始寻找是否还有其他的替代方案,来解决 Java 线程切换代价高的问题。

响应式编程的替代方案

上文提到了,当前的 Web 应用程序,代码的执行时间和 Java 线程之间切换的时间不成比例。Java 的线程是以 1:1 的形式分配给操作系统的。尽管 Java 有线程池的方式来帮你在不进行上下文切换的情况下执行代码,但是终究只是一个笨拙的解决方式。


类似于 C#、JavaScript、Kotlin 等这一类的编程语言在这个领域已经走在了前面,他们在编程语言中集成了这部分功能,可以帮助你执行一小段异步代码,并在返回的时候阻塞,直到执行结束。在 C#和 JavaScript 中,可以使用 Async 和 Await 关键字,在 Go 和 Kotlin 中,同样包括了协程的概念来提供类似的功能。


以上这些概念的想法都是类似的,如果我发现需要执行一个比较长时间的请求(例如:访问数据库),代码不应该阻塞住,而是在响应结束并返回结果的时候,指定执行需要的代码,并且实现代码要简单易读,方便调试和测试。

Java 1 中的绿色线程

所有的努力都是为了保证开发人员能够轻松地开发、测试、维护的调试响应式程序代码,但是关键问题在于,响应式编程在运行时机制下,是否能够提供更好的性能。上面提到的一些替代方案里面,响应式编程又是否是解决问题的正确方法?虽然以上问题,目前无法得到一个确切的答案,但是,其他语言的替代方案(Asyn,Await 和协程)是添加在编程语言内部的,并不是第三方类库。那么 Java 中额外的第三方类库是不是有点多余呢?


我们先来回顾一下 Java 的历史,了解一些有趣的背景故事:


Java 1.1 的版本中,整个线程模型的实现被称为“绿色线程”,Java 虚拟机只在单核系统中执行,Java 线程在虚拟机中使用自己的调度算法。在 Java 的虚拟内存管理下,线程切换和上下文切换非常快,几乎没有内存开销。


这个方式的优点是,Java 程序中同步访问数据的操作并不复杂,根本不会出现对变量真正的并行访问,所有的内容都在一个系统进程里面执行,因此可以说这只是一个“虚拟”的并行。


当然,问题也随之而来,这类程序在多核系统里面,只能使用一个系统内核,根本无法发挥计算机的整体性能。在实践中,这个缺点被不断地放大,甚至足以掩盖它的优点。


因此,绿色线程的想法很快就被终止了,在 Java1.2 中,提供了一个切换绿色线程和本地线程(详情链接)的开关。在 Java1.3 中,就只支持本地线程了。直到今天,所有的计算机内核都可以得以利用。

Project loom 结合了线程模型

Project Loom是在去年推出,JVM 背后的想法就是重新提供类似绿色线程的支持。但与过去不同的是,这些内容不是为了取代当前操作系统的线程模式,而是更好的支持它。因此两个线程模型会在 JVM 虚拟机中并存,而且可以在程序中同时使用。


操作系统线程继续由 Java 的 Thread 类实现,而新的绿色线程,则交由 Fiber 类管理。如果有必要的话,他们可以拥有一个共同的基类,这样现有的代码就不需要为了 Fiber 而修改代码,也不需要知道线程背后的情况。在 Java 虚拟内存的管理下,通过 Fiber 切换上下文几乎没有开销,同步也应该会变得更高效。有一个实现思路是,调度程序确保一个线程里面执行两个彼此相互依赖(比如说访问相同的变量)的 Fibers,这样的话它们就永远不会并行执行,也没有了同步的必要。


为了实现 Fibers,Java 的执行线程将会被分为两部分:执行过程和调度。执行过程标记了执行状态,例如上下文要执行代码的状态,包括调用参数、堆栈等。而调度则负责匀速的执行所有要执行的部分。


将调度程序与执行过程分离的好处是,目前调度和执行的上下文都由操作系统管理,我们无法干预。而分离之后,JVM 可以控制只执行其中的一个或者多个。因此,绿色线程(Fibers)可以只由 Java 自己实现,且 Java 既有的调度器也可以被重用(例如 Fork-Join 池)。


还有一个有趣的优点是,分离之后,执行过程包含的内容可以作为一个 Java API 开放给每个开发者,并成为 Java 语言的一个特性。

结论

程序通过操作系统线程,每个请求一个线程执行的模式,带来了很大的性能问题,这个问题被响应式编程的方式解决了。但与此同时,也带来了很多开发和维护上的问题,因为响应式编程的代码更难测试和维护。


操作系统中进程的切换带来了很大的损失,绿色线程是一个比较可行的解决方案。但是在 Java1.3 之后就不再支持了,主要原因是它不能在多核系统上执行。Project Loom 是绿色线程的一个变种,这个提议会伴随着 Java 过程执行的理念,作为一个附属品提供给开发者。这部分内容也是参考了其他编程语言的特性,例如 Go 和 Kotlin 的协程。


针对 Project Loom,大家可能比较关心它何时集成到 JDK 中?会对响应式编程产生多大的影响?因为单从性能角度上来看,相比响应式编程,它对于 Java 生态的意义不大。而这些内容,我们还得再观察一段时间,才能有定论。


原文链接:The fight for performance – Is reactive programming the right approach?


2019-04-24 06:2849708

评论 4 条评论

发布
用户头像
真的,用了reactor之后我觉得有些难受,响应式在后端一定会成为一个过渡技术,终极还是协程啊
2019-05-15 21:16
回复
用户头像
callback/promise这种把业务逻辑打散到不同的回调中,而且加上java这个半残的lamda,很难说这就是未来啊。Reactor也是这种。

至于async/await,确实能够写的很爽,debug的话,有debugger配合,问题也不大。但是现有代码要做很多修改才能跑起来,一处用await,处处用await,成本太高。唉~
2019-05-02 23:41
回复
用户头像
如果类似js加入await不就搞定了,方便编写也方便维护
2019-04-24 09:14
回复
作为C#程序员,我也同意,哈哈。
2019-04-24 10:09
回复
没有更多了
发现更多内容

Java教程视频百度网盘,小甲鱼数据结构百度云,腾讯Java面试题

Java 程序员 后端

Java爬虫爬取视频,尚硅谷笔试答案,最全面试考点与面试技巧

Java 程序员 后端

Java开发面试题!牛客网java开发高频面试题,让我成功在寒冬中站稳脚步

Java 程序员 后端

Java技术成长,kafka学习教程,Java开发者面试如何系统复习

Java 程序员 后端

Java数据处理的常用技术,springboot源码解读与原理分析

Java 程序员 后端

Java程序员如何有效提升学习效率,如何化身BAT面试收割机

Java 程序员 后端

Java百度云资源,java基础案例教程黑马程序员在线阅读,美团Java面试流程

Java 程序员 后端

Java研发岗面试复盘总,4面技术5面HR附加笔试面

Java 程序员 后端

Java程序员全套,百度三面牛客网猿生活,疯狂膜拜

Java 程序员 后端

Java技术基础知识总结,菜鸟教程mysql,Java重要知识点

Java 程序员 后端

Java教程百度云最新版,java写脚本教程视频,程序员必须要了解的知识点

Java 程序员 后端

Java架构师必备技能,java程序设计实用教程第五版答案,掌握这个提升路径

Java 程序员 后端

Java框架,黑马java视频教程,面试资料分享

Java 程序员 后端

Java知识体系!极客学院黑马程序员,BIO和NIO有啥区别

Java 程序员 后端

Java研发岗必问30+道高级面试题,腾讯,字节等大厂面试真题汇总

Java 程序员 后端

Java程序员必会!开课吧java高级架构师课程,Java开发大厂面试经验

Java 程序员 后端

阿里云重磅发布业务中台产品 BizWorks,中台发展进入下一个阶段

阿里巴巴云原生

阿里云 云原生 业务中台 云栖大会 BizWorks

Java日常开发的12个坑,你踩过几个,一招让你拿下seata分布式事务框架

Java 程序员 后端

Java春招实习面试经验汇总,图灵学院诸葛,Java微服务架构视频下载

Java 程序员 后端

Java百度云,springboot实例教程,面试大厂应该注意哪些问题

Java 程序员 后端

Java微服务架构图,nginx视频教程百度云,学习指南

Java 程序员 后端

Java性能优化最佳实践,mybatis入门视频

Java 程序员 后端

Java知识体系!java黑马视频和达内,链表反转的两种实现方法

Java 程序员 后端

Java排序算法面试,黑马java项目一,springboot实战项目源码

Java 程序员 后端

Java月薪过万要掌握的技能,javajdk下载教程,高级Java工程师面试问题

Java 程序员 后端

Java架构师进阶之路,马士兵的java教程,大厂Java面试总结+详细解答

Java 程序员 后端

Java的Io模型你了解多少?尚硅谷大厂学院课,Java开发面试笔试题大汇总

Java 程序员 后端

Java教程百度云最新版,极客时间vip年卡,Java开发者面试如何系统复习

Java 程序员 后端

Java数据结构面试题,java架构师指南下载百度,Java工程师面试题及答案

Java 程序员 后端

云栖发布|企业级互联网架构全新升级 ,助力数字创新

阿里巴巴云原生

阿里云 云原生 产品升级 云栖大会

Java百度云教程,深入java虚拟机百度云,附详细答案

Java 程序员 后端

性能之争:响应式编程真的有效吗?_编程语言_Arne Limburg_InfoQ精选文章