Java高并发革命!JDK19新特性——虚拟线程(Virtual Threads)

介绍

虚拟线程具有和 Go 语言的 goroutines 和 Erlang 语言的进程类似的实现方式,它们是用户模式(user-mode)线程的一种形式。

在过去 Java 中常常使用线程池来进行平台线程的共享以提高对计算机硬件的使用率,但在这种异步风格中,请求的每个阶段可能在不同的线程上执行,每个线程以交错的方式运行属于不同请求的阶段,与 Java 平台的设计不协调从而导致:

  • 堆栈跟踪不提供可用的上下文
  • 调试器不能单步执行请求处理逻辑
  • 分析器不能将操作的成本与其调用方关联。

而虚拟线程既保持与平台的设计兼容,同时又能最佳地利用硬件从而不影响可伸缩性。虚拟线程是由 JDK 而非操作系统提供的线程的轻量级实现

  • 虚拟线程是没有绑定到特定操作系统线程的线程。
  • 平台线程是以传统方式实现的线程,作为围绕操作系统线程的简单包装

摘要

向 Java 平台引入虚拟线程。虚拟线程是轻量级线程,它可以大大减少编写、维护和观察高吞吐量并发应用程序的工作量。

目标

  • 允许以简单的每个请求一个线程的方式编写的服务器应用程序以接近最佳的硬件利用率进行扩展。
  • 允许使用 java.lang.ThreadAPI 的现有代码采用虚拟线程,并且只做最小的更改。
  • 使用现有的 JDK 工具可以方便地对虚拟线程进行故障排除、调试和分析。

非目标

  • 移除线程的传统实现或迁移现有应用程序以使用虚拟线程并不是目标。
  • 改变 Java 的基本并发模型。
  • 我们的目标不是在 Java 语言或 Java 库中提供新的资料平行结构。StreamAPI 仍然是并行处理大型数据集的首选方法。

动机

近30年来,Java 开发人员一直依赖线程作为并发服务器应用程序的构件。每个方法中的每个语句都在一个线程中执行,而且由于 Java 是多线程的,因此执行的多个线程同时发生。

线程是 Java 的并发单元: 一段顺序代码,与其他这样的单元并发运行,并且在很大程度上独立于这些单元。

每个线程都提供一个堆栈来存储本地变量和协调方法调用,以及出错时的上下文: 异常被同一个线程中的方法抛出和捕获,因此开发人员可以使用线程的堆栈跟踪来查找发生了什么。

线程也是工具的一个核心概念: 调试器遍历线程方法中的语句,分析器可视化多个线程的行为,以帮助理解它们的性能。

两种并发 style

thread-per-request style

  • 服务器应用程序通常处理彼此独立的并发用户请求,因此应用程序通过在整个请求持续期间为该请求分配一个线程来处理请求是有意义的。这种按请求执行线程的 style 易于理解、易于编程、易于调试和配置,因为它使用平台的并发单元来表示应用程序的并发单元

  • 服务器应用程序的可伸缩性受到利特尔定律(Little's Law)的支配,该定律关系到延迟并发性吞吐量: 对于给定的请求处理持续时间(延迟)应用程序同时处理的请求数(并发性) 必须与到达速率(吞吐量) 成正比增长。

    例如,假设一个平均延迟为 50ms 的应用程序通过并发处理 10 个请求实现每秒 200 个请求的吞吐量。为了使该应用程序的吞吐量达到每秒 2000 个请求,它将需要同时处理 100 个请求。如果在请求持续期间每个请求都在一个线程中处理,那么为了让应用程序跟上,线程的数量必须随着吞吐量的增长而增长

  • 不幸的是,可用线程的数量是有限的,因为 JDK 将线程实现为操作系统(OS)线程的包装器。操作系统线程代价高昂,因此我们不能拥有太多线程,这使得实现不适合每个请求一个线程的 style 。

  • 如果每个请求在其持续时间内消耗一个线程,从而消耗一个 OS 线程,那么线程的数量通常会在其他资源(如 CPU 或网络连接)耗尽之前很久成为限制因素。JDK 当前的线程实现将应用程序的吞吐量限制在远低于硬件所能支持的水平。即使在线程池中也会发生这种情况,因为池有助于避免启动新线程的高成本,但不会增加线程的总数

asynchronous style

  • 一些希望充分利用硬件的开发人员已经放弃了每个请求一个线程(thread-per-request) 的 style ,转而采用线程共享(thread-sharing ) 的 style 。

  • 请求处理代码不是从头到尾处理一个线程上的请求,而是在等待 I/O 操作完成时将其线程返回到一个池中,以便该线程能够处理其他请求。这种细粒度的线程共享(其中代码只在执行计算时保留一个线程而不是在等待 I/O 时保留该线程)允许大量并发操作,而不需要消耗大量线程。

  • 虽然它消除了操作系统线程的稀缺性对吞吐量的限制,但代价很高: 它需要一种所谓的异步编程 style ,采用一组独立的 I/O 方法,这些方法不等待 I/O 操作完成,而是在以后将其完成信号发送给回调。如果没有专门的线程,开发人员必须将请求处理逻辑分解成小的阶段,通常以 lambda 表达式的形式编写,然后将它们组合成带有 API 的顺序管道(例如,参见 CompletableFuture,或者所谓的“反应性”框架)。因此,它们放弃了语言的基本顺序组合运算符,如循环和 try/catch 块。

  • 在异步样式中,请求的每个阶段可能在不同的线程上执行,每个线程以交错的方式运行属于不同请求的阶段。这对于理解程序行为有着深刻的含义:

    • 堆栈跟踪不提供可用的上下文
    • 调试器不能单步执行请求处理逻辑
    • 分析器不能将操作的成本与其调用方关联。
  • 当使用 Java 的流 API 在短管道中处理数据时,组合 lambda 表达式是可管理的,但是当应用程序中的所有请求处理代码都必须以这种方式编写时,就有问题了。这种编程 style 与 Java 平台不一致,因为应用程序的并发单元(异步管道)不再是平台的并发单元

对比

thread-per-request style asynchronous style
优点 与 Java 平台的设计相协调,易于理解、易于编程、易于调试和配置 这种细粒度的线程共享允许大量并发操作,而不需要消耗大量线程,提高可伸缩性
缺点 可用线程的数量是有限的,操作系统线程代价高昂,因此我们不能拥有太多线程,这使得实现不适合 thread-per-request style 这种编程 style 与 Java 平台不一致,因为应用程序的并发单元(异步管道)不再是平台的并发单元

使用虚拟线程保留thread-per-request style

为了使应用程序能够在与平台保持和谐的同时进行扩展,我们应该通过更有效地实现线程来努力保持每个请求一个线程的 style ,以便它们能够更加丰富。

操作系统无法更有效地实现 OS 线程,因为不同的语言和运行时以不同的方式使用线程堆栈。然而,Java 运行时实现 Java 线程的方式可以切断它们与操作系统线程之间的一一对应关系。正如操作系统通过将大量虚拟地址空间映射到有限数量的物理 RAM 而给人一种内存充足的错觉一样,Java 运行时也可以通过将大量虚拟线程映射到少量操作系统线程而给人一种线程充足的错觉

  • 虚拟线程是没有绑定到特定操作系统线程的线程。
  • 平台线程是以传统方式实现的线程,作为围绕操作系统线程的简单包装

thread-per-request 样式的应用程序代码可以在整个请求期间在虚拟线程中运行,但是虚拟线程只在 CPU 上执行计算时使用操作系统线程。其结果是与异步样式相同的可伸缩性,除了它是透明实现的:

当在虚拟线程中运行的代码调用 Java.* API 中的阻塞 I/O 操作时,运行时执行一个非阻塞操作系统调用,并自动挂起虚拟线程,直到稍后可以恢复。

对于 Java 开发人员来说,虚拟线程是创建成本低廉、数量几乎无限多的线程。硬件利用率接近最佳,允许高水平的并发性,从而提高吞吐量,而应用程序仍然与 Java 平台及其工具的多线程设计保持协调。

虚拟线程的意义

虚拟线程是廉价丰富的,因此永远不应该被共享(即使用线程池) : 应该为每个应用程序任务创建一个新的虚拟线程。

因此,大多数虚拟线程的寿命都很短,并且具有浅层调用堆栈,执行的操作只有单个 HTTP 客户机调用或单个 JDBC 查询那么少。相比之下,平台线程重量级昂贵的,因此经常必须共享。它们往往是长期存在的,具有深度调用堆栈,并且在许多任务之间共享。

总之,虚拟线程保留了可靠的 thread-per-request style ,这种 style 与 Java 平台的设计相协调,同时又能最佳地利用硬件。使用虚拟线程并不需要学习新的概念,尽管它可能需要为应对当今线程的高成本而养成的忘却习惯。虚拟线程不仅可以帮助应用程序开发人员ーー它们还可以帮助框架设计人员提供易于使用的 API,这些 API 与平台的设计兼容,同时又不影响可伸缩性

说明

如今&#

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值