本篇文章给大家带来了关于java的相关知识,其中主要介绍了关于jdk19中虚拟线程的相关内容,虚拟线程是具有和go语言的goroutines 和 Erlang 语言的进程类似的实现方式,它们是用户模式线程的一种形式,下面一起来看一下,希望对大家有帮助。
程序员必备接口测试调试工具:立即使用
Apipost = Postman + Swagger + Mock + Jmeter
Api设计、调试、文档、自动化测试工具
后端、前端、测试,同时在线协作,内容实时同步
推荐学习:《java视频教程》
介绍
虚拟线程具有和 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
为了使应用程序能够在与平台保持和谐的同时进行扩展,我们应该通过更有效地实现线程来努力保持每个请求一个线程的 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 与平台的设计兼容,同时又不影响可伸缩性。
说明
如今,java.lang 的每一个实例。JDK 中的线程是一个平台线程。平台线程在底层操作系统线程上运行 Java 代码,并在代码的整个生命周期中捕获操作系统线程。平台线程的数量仅限于操作系统线程的数量。
虚拟线程是 java.lang 的一个实例。在基础操作系统线程上运行 Java 代码,但在代码的整个生命周期中不捕获该操作系统线程的线程。这意味着许多虚拟线程可以在同一个 OS 线程上运行它们的 Java 代码,从而有效地共享它们。平台线程垄断了一个珍贵的操作系统线程,而虚拟线程却没有。虚拟线程的数量可能比操作系统线程的数量大得多。
虚拟线程是由 JDK 而非操作系统提供的线程的轻量级实现。它们是用户模式(user-mode)线程的一种形式,已经在其他多线程语言中取得了成功(例如,Go 中的 goroutines 和 Erlang 的进程)。在 Java 的早期版本中,用户模式线程甚至以所谓的“绿线程”为特色,当时 OS 线程还不成熟和普及。然而,Java 的绿色线程都共享一个 OS 线程(M: 1调度) ,并最终被平台线程超越,实现为 OS 线程的包装器(1:1调度)。虚拟线程采用 M: N 调度,其中大量(M)虚拟线程被调度在较少(N)操作系统线程上运行。
虚拟线程 VS 平台线程
简单示例
开发人员可以选择使用虚拟线程还是平台线程。下面是一个创建大量虚拟线程的示例程序。该程序首先获得一个 ExecutorService,它将为每个提交的任务创建一个新的虚拟线程。然后,它提交10000项任务,等待所有任务完成:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 10000).forEach(i -> { executor.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); return i; }); }); } // executor.close() is called implicitly, and waits
本例中的任务是简单的代码(休眠一秒钟) ,现代硬件可以轻松支持10,000个虚拟线程并发运行这些代码。在幕后,JDK 在少数操作系统线程上运行代码,可能只有一个线程。
如果这个程序使用 ExecutorService 为每个任务创建一个新的平台线程,比如 Executors.newCachedThreadPool () ,那么情况就会大不相同。ExecutorService 将尝试创建10,000个平台线程,从而创建10,000个 OS 线程,程序可能会崩溃,这取决于计算机和操作系统。
相反,如果程序使用从池中获取平台线程的 ExecutorService (例如 Executors.newFixedThreadPool (200)) ,情况也不会好到哪里去。ExecutorService 将创建200个平台线程,由所有10,000个任务共享,因此许多任务将按顺序运行,而不是并发运行,而且程序将需要很长时间才能完成。对于这个程序,一个有200个平台线程的池只能达到每秒200个任务的吞吐量,而虚拟线程达到每秒10,000个任务的吞吐量(在充分预热之后)。此外,如果示例程序中的10000被更改为1000000,那么该程序将提交1,000,000个任务,创建1,000,000个并发运行的虚拟线程,并且(在足够的预热之后)实现大约1,000,000任务/秒的吞吐量。
如果这个程序中的任务执行一秒钟的计算(例如,对一个巨大的数组进行排序)而不仅仅是休眠,那么增加超出处理器核心数量的线程数量将无济于事,无论它们是虚拟线程还是平台线程。
虚拟线程并不是更快的线程ーー它们运行代码的速度并不比平台线程快。它们的存在是为了提供规模(更高的吞吐量) ,而不是速度(更低的延迟) 。它们的数量可能比平台线程多得多,因此根据 Little’s Law,它们能够实现更高吞吐量所需的更高并发性。
换句话说,虚拟线程可以显著提高应用程序的吞吐量,在如下情况时:
-
并发任务的数量很多(超过几千个)
-
工作负载不受 CPU 限制,因为在这种情况下,比处理器核心拥有