写点什么

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

  • 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:2849566

评论 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
回复
没有更多了
发现更多内容

2021 斩获 90K 月薪的 Spring 全家桶:文档 + 面试题 + 学习笔记 + 思维导图

编程 架构 微服务 IT 计算机

万字长文讲透低代码

百度开发者中心

最佳实践 开发者 方法论 低代码 语言 & 开发

Java 程序性能优化“学习日记”

Java 编程 面试 IT 计算机

你真的懂语音特征吗?

华为云开发者联盟

语音 音频 声学 时域图 时域

孩子排斥写作业 VS 员工不接活儿——项目管理来帮忙

Ian哥

台达AS228T_CanOpen_VFD_X

林建

台达 AS228T Canopen 功能块 E变址

面试进阶齐飞!霸榜GitHub的 Java 全栈笔记太香了!

Java 编程 程序员 IT 计算机

我看 JAVA 之 并发编程【三】java.util.concurrent.atomic

awen

Java 并发编程 Atomic 原子操作

这波性能优化,太炸裂了!

why技术

Java 性能优化 JVM

高频面试题-请把Java垃圾回收器说清楚

Java 编程 架构 面试 JVM

spring cloud 在国内中小型公司能用起微服务来吗?

Java 程序员 架构 面试 IT

使用Micronaut框架构建一个微服务网络.

Java 编程 架构 面试 程序人生

Spring Boot 实战派,让开发像喝水一样简单!

Java 程序员 架构 面试 IT

Java测试框架九大法宝

FunTester

自动化测试 JUnit 测试框架 selenium testNG

惠及百万用户 医保“上云”有了新思路

云计算

MySQL中的DEFINER(定义者)是什么

Simon

MySQL

Tensor:Pytorch神经网络界的Numpy

华为云开发者联盟

神经网络 数组 PyTorch Numpy Tenso

“助力金九银十”25 大Java后端面试指南,3000道面试题解析

Java 编程 程序员 面试 IT

毕业六年本科,去年疫情期间备战二个月,阿里巴巴四面成功!定级 P7

Java 程序员 架构 面试 IT

Compose 编程思想

Changing Lin

8月日更

如何用Camtasia添加视频水印?

淋雨

视频剪辑 Camtasia 录屏软件

xposed 入门之修改手机 IMEI

Qunar技术沙龙

android 程序员 App 经验分享 安卓

经验分享:我是如何拿下微软、滴滴、百度等 20家大厂的 Offer?

Java 程序员 架构 面试 IT

从技术到文案,还回技术么?

escray

学习 极客时间 朱赟的技术管理课 8月日更

接口返回值一定不允许使用枚举类型吗?

skow

Java 面试 后端 开发规范

算法有救了!GitHub 上神仙项目手把手带你刷算法,Star 数已破500k

Java 编程 程序员 面试 算法

C++ Vector

若尘

c++ vector 8月日更

为什么安全性在托管中变得越来越重要

九河云安全

收获颇丰!这份阿里架构师纯手敲JDK源码全彩小册可以打满分

Java架构追梦

Java 阿里巴巴 架构 面试 jdk源码

Apache之道在腾讯的探索与实践

腾源会

Apache 开源 腾源会 腾讯开源

Python代码阅读(第7篇):列表分组计数

Felix

Python 编程 Code Programing 阅读代码

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