响应式编程又变天了?看JDK21虚拟线程如何颠覆!

【响应式编程又变天了?看JDK21虚拟线程如何颠覆!】命令式风格编程一直深受开发者喜爱,如 if-then-else、while 循环、函数和代码块等结构使代码易理解、调试,异常易追踪 。然而 , 像所有好的东西一样,通常也有问题 。这种编程风格导致线程被阻塞时间远超过必要时间 。
1 同步阻塞设计1.1 同步阻塞设计的线程图为了便于你理解,让我们看一个典型的企业用例请求:

  • 从DB获取数据
  • 从 Web 服务获取数据
  • 合并结果并将最终合并的结果发送回用户
在像 Tomcat 这样的应用服务器中,一个平台线程将专用于用户请求,该线程将继续调用从数据库获取数据的代码(调用 FetchDataFromDB),然后调用从 Web 服务获取数据的代码(调用 FetchDataFromService),然后继续合并并将数据发送给用户(调用 SendDataToUser) 。
如下图,将执行线程从上到下表示为一个垂直箭头:
  • 绿色是执行的 CPU 部分
  • 红色是线程等待数据的时间
大多企业应用都是 IO 绑定的,因此线程在大多时间内实际是浪费资源 。
响应式编程又变天了?看JDK21虚拟线程如何颠覆!

文章插图
图片
1.2 评估JAVA 中 , 平台线程是昂贵资源 , 因为默认 , 每个平台线程消耗 1MB 栈内存 。即 JVM 中运行的平台线程数量有上限 。因此,若一个平台线程专用于用户请求,对高并发用户的应用程序,就带来问题 。传统解决方案是创建一个具有最大线程数的线程池,并根据需要水平或垂直扩展应用程序:
  • 垂直扩展意味着向容器或 VM 添加更多资源
  • 水平扩展则意味着添加应用程序的更多实例
2 异步阻塞设计2.1 异步阻塞设计线程图为了提高性能,可用异步模型 , 并行运行一些串行任务 。如若假设数据库和 Web 服务的获取任务可以并行运行,那么它们可以在各自的平台线程中执行 。
响应式编程又变天了?看JDK21虚拟线程如何颠覆!

文章插图
图片
用户请求线程启动两个线程:
  • 一个用于处理从数据库获取数据
  • 另一个用于从 Web 服务获取数据
  • 然后,它将阻塞以获取两者结果,然后继续合并并将数据发送给用户
在 Java 可通过向 Executor Service 提交 Callable 或 Runnable 任务并使用 Java Futures 来实现 。
2.2 评估这将提高性能,因为两个数据获取是并行执行的 。但是 , 即使在大多数时间内可能会有性能提升,但是在短时间内 , 平台线程的数量现在从 1 增加到 3 。从可扩展性看,在那段时间内情况更坏 。
3 响应式样式设计响应式编程设计就是为解决这问题 。
3.1 部分响应式设计线程图在于昂贵的平台线程在阻塞操作期间浪费大部分时间 。随 Servlet 3.0 和 3.1 引入,Servlet 线程在发送 HTTP 数据回用户时无需保持活动状态,这为更巧妙编程打开解决线程阻塞的大门 。Java 8 CompletableFuture类可在其中创建响应式管道 。这种开发风格思想是为该用例指定一个执行管道,而非执行用例本身 。
用户请求线程只需指定用例的 CompletableFuture 管道(或任何其他管道),并在尽可能短的时间内将其释放回线程池(因为无需再保持活动状态以向用户发送数据) 。
响应式编程又变天了?看JDK21虚拟线程如何颠覆!

文章插图
图片
此时,用户请求线程创建一个运行 3 个活动的管道:
  • 先并行运行 FetchDataFromService、FetchDataFromDB
  • 再运行 Send2User
但创建此管道后,用户请求线程将被简单释放回线程池 。大大减轻 JVM 负担,因为现在它有一个较少的线程要处理 。一旦数据提取线程完成其执行,数据将被发送给用户 。
评估但这只是部分解决问题,因为从 Web 服务、DB获取数据的实际活动仍是在它们各自的平台线程中阻塞 。这带来问题:SE须确保他从管道中生成的任务不是阻塞的 。这很难做到,因为它是手动完成的,并且肯定是错误的,因为在编译时或运行时它不会被标记为警告或错误 。


推荐阅读