Virtual Threads 虚拟线程
原文:https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html
虚拟线程是轻量级线程,可以减少编写、维护和调试高吞吐量并发应用程序的工作量。
有关虚拟线程的背景信息,请参阅 JEP 444。
线程是可调度的最小处理单元。它与其他此类单元同时运行,并且基本上独立运行。它是 java.lang.Thread
的一个实例。线程有两种,平台线程和虚拟线程。
1. What is a Platform Thread?
1. 什么是平台线程?
平台线程被实现为操作系统 (OS) 线程的薄包装器。平台线程在其底层操作系统线程上运行 Java 代码,并且平台线程在平台线程的整个生命周期内捕获其操作系统线程。因此,可用平台线程的数量仅限于操作系统线程的数量。
平台线程通常具有大型线程堆栈和由操作系统维护的其他资源。它们适合运行所有类型的任务,但资源可能有限。
2. What is a Virtual Thread?
2. 什么是虚拟线程?
与平台线程一样,虚拟线程也是 java.lang.Thread
的实例。但是,虚拟线程并不依赖于特定的操作系统线程。虚拟线程仍然在操作系统线程上运行代码。但是,当虚拟线程中运行的代码调用阻塞 I/O 操作时,Java 运行时会挂起虚拟线程,直到可以恢复为止。与挂起的虚拟线程关联的操作系统线程现在可以自由地为其他虚拟线程执行操作。
虚拟线程的实现方式与虚拟内存类似。为了模拟大量内存,操作系统将较大的虚拟地址空间映射到有限的 RAM。同样,为了模拟大量线程,Java运行时将大量虚拟线程映射到少量操作系统线程。
与平台线程不同,虚拟线程通常具有浅调用堆栈,只执行单个 HTTP 客户端调用或单个 JDBC 查询。尽管虚拟线程支持线程局部变量和可继承的线程局部变量,但您应该仔细考虑使用它们,因为单个 JVM 可能支持数百万个虚拟线程。
虚拟线程适合运行大部分时间处于阻塞状态、通常等待 I/O 操作完成的任务。但是,它们不适用于长时间运行的 CPU 密集型操作。
3. Why Use Virtual Threads?
3. 为什么使用虚拟线程?
在高吞吐量并发应用程序中使用虚拟线程,尤其是那些包含大量并发任务且大部分时间都在等待的应用程序。服务器应用程序是高吞吐量应用程序的示例,因为它们通常处理许多执行阻塞 I/O 操作(例如获取资源)的客户端请求。
虚拟线程并不是更快的线程;它们运行代码的速度并不比平台线程快。它们的存在是为了提供规模(更高的吞吐量),而不是速度(更低的延迟)。
4. Creating and Running a Virtual Thread
4. 创建并运行虚拟线程
Thread
和 Thread.Builder
API 提供了创建平台线程和虚拟线程的方法。 java.util.concurrent.Executors
类还定义了创建 ExecutorService
的方法,该 ExecutorService
为每个任务启动一个新的虚拟线程。
4.1 Creating a Virtual Thread with the Thread Class and the Thread.Builder Interface
4.1 使用 Thread 类和 Thread.Builder 接口创建虚拟线程
调用 Thread.ofVirtual()
方法创建 Thread.Builder
实例,用于创建虚拟线程。
以下示例创建并启动一个打印消息的虚拟线程。它调用 join 方法来等待虚拟线程终止。 (这使您能够在主线程终止之前看到打印的消息。)
1 |
|
Thread.Builder
接口允许您创建具有常见 Thread
属性(例如线程名称)的线程。 Thread.Builder.OfPlatform
子接口创建平台线程,而 Thread.Builder.OfVirtual
创建虚拟线程。
以下示例使用 Thread.Builder
接口创建名为 MyThread
的虚拟线程:
1 |
|
以下示例使用 Thread.Builder
创建并启动两个虚拟线程:
1 |
|
此示例打印类似于以下内容的输出:
1 |
|
4.2 Creating and Running a Virtual Thread with the Executors.newVirtualThreadPerTaskExecutor() Method
使用 Executors.newVirtualThreadPerTaskExecutor() 方法创建并运行虚拟线程
执行器允许您将线程管理和创建与应用程序的其余部分分开。
以下示例使用 Executors.newVirtualThreadPerTaskExecutor()
方法创建 ExecutorService
。每当调用 ExecutorService.submit(Runnable)
时,都会创建一个新的虚拟线程并开始运行该任务。此方法返回 Future
的实例。请注意,方法 Future.get()
等待线程的任务完成。因此,一旦虚拟线程的任务完成,此示例就会打印一条消息。
1 |
|
4.3 Multithreaded Client Server Example
多线程客户端服务器示例
以下示例由两个类组成。 EchoServer
是一个服务器程序,它监听端口并为每个连接启动一个新的虚拟线程。 EchoClient
是一个客户端程序,它连接到服务器并发送在命令行上输入的消息。
1 |
|
EchoClient
创建一个套接字,从而获得到 EchoServer
的连接。它在标准输入流上读取用户的输入,然后通过将文本写入套接字来将该文本转发到 EchoServer
。 EchoServer
将输入通过套接字回显到 EchoClient
。 EchoClient
读取并显示从服务器传回的数据。 EchoServer
可以通过虚拟线程同时服务多个客户端,每个客户端连接一个线程。
1 |
|
5. Scheduling Virtual Threads and Pinned Virtual Threads
调度虚拟线程和固定虚拟线程
操作系统调度平台线程何时运行。但是,Java 运行时会调度虚拟线程的运行时间。当Java运行时调度虚拟线程时,它会在平台线程上分配或安装虚拟线程,然后操作系统照常调度该平台线程。该平台线程称为载体。运行一些代码后,虚拟线程可以从其载体上卸载。这通常发生在虚拟线程执行阻塞 I/O 操作时。虚拟线程从其载体上卸载后,载体就空闲了,这意味着Java运行时调度程序可以在其上挂载不同的虚拟线程。
当虚拟线程固定到其载体时,无法在阻塞操作期间卸载该虚拟线程。虚拟线程在以下情况下被固定:
虚拟线程在
synchronized
块或方法内运行代码虚拟线程运行
native
方法或外部函数(请参阅外部函数和内存 API)
固定不会使应用程序不正确,但可能会妨碍其可扩展性。尝试通过修改频繁运行的 synchronized
块或方法并使用 java.util.concurrent.locks.ReentrantLock
保护潜在的长时间 I/O 操作来避免频繁且长期的固定。