原文: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. 创建并运行虚拟线程

ThreadThread.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 方法来等待虚拟线程终止。 (这使您能够在主线程终止之前看到打印的消息。)

Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello"));
thread.join();

Thread.Builder 接口允许您创建具有常见 Thread 属性(例如线程名称)的线程。 Thread.Builder.OfPlatform 子接口创建平台线程,而 Thread.Builder.OfVirtual 创建虚拟线程。

以下示例使用 Thread.Builder 接口创建名为 MyThread 的虚拟线程:

Thread.Builder builder = Thread.ofVirtual().name("MyThread");
Runnable task = () -> {
    System.out.println("Running thread");
};
Thread t = builder.start(task);
System.out.println("Thread t name: " + t.getName());
t.join();

以下示例使用 Thread.Builder 创建并启动两个虚拟线程:

Thread.Builder builder = Thread.ofVirtual().name("worker-", 0);
Runnable task = () -> {
    System.out.println("Thread ID: " + Thread.currentThread().threadId());
};

// name "worker-0"
Thread t1 = builder.start(task);   
t1.join();
System.out.println(t1.getName() + " terminated");

// name "worker-1"
Thread t2 = builder.start(task);   
t2.join();  
System.out.println(t2.getName() + " terminated");

此示例打印类似于以下内容的输出:

Thread ID: 21
worker-0 terminated
Thread ID: 24
worker-1 terminated

4.2 Creating and Running a Virtual Thread with the Executors.newVirtualThreadPerTaskExecutor() Method

使用 Executors.newVirtualThreadPerTaskExecutor() 方法创建并运行虚拟线程

执行器允许您将线程管理和创建与应用程序的其余部分分开。

以下示例使用 Executors.newVirtualThreadPerTaskExecutor() 方法创建 ExecutorService 。每当调用 ExecutorService.submit(Runnable) 时,都会创建一个新的虚拟线程并开始运行该任务。此方法返回 Future 的实例。请注意,方法 Future.get() 等待线程的任务完成。因此,一旦虚拟线程的任务完成,此示例就会打印一条消息。

try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<?> future = myExecutor.submit(() -> System.out.println("Running thread"));
    future.get();
    System.out.println("Task completed");
    // ...

4.3 Multithreaded Client Server Example

多线程客户端服务器示例

以下示例由两个类组成。 EchoServer 是一个服务器程序,它监听端口并为每个连接启动一个新的虚拟线程。 EchoClient 是一个客户端程序,它连接到服务器并发送在命令行上输入的消息。

public class EchoServer {
    
    public static void main(String[] args) throws IOException {
         
        if (args.length != 1) {
            System.err.println("Usage: java EchoServer <port>");
            System.exit(1);
        }
         
        int portNumber = Integer.parseInt(args[0]);
        try (
            ServerSocket serverSocket =
                new ServerSocket(Integer.parseInt(args[0]));
        ) {                
            while (true) {
                Socket clientSocket = serverSocket.accept();
                // Accept incoming connections
                // Start a service thread
                Thread.ofVirtual().start(() -> {
                    try (
                        PrintWriter out =
                            new PrintWriter(clientSocket.getOutputStream(), true);
                        BufferedReader in = new BufferedReader(
                            new InputStreamReader(clientSocket.getInputStream()));
                    ) {
                        String inputLine;
                        while ((inputLine = in.readLine()) != null) {
                            System.out.println(inputLine);
                            out.println(inputLine);
                        }
                    
                    } catch (IOException e) { 
                        e.printStackTrace();
                    }
                });
            }
        } catch (IOException e) {
            System.out.println("Exception caught when trying to listen on port "
                + portNumber + " or listening for a connection");
            System.out.println(e.getMessage());
        }
    }
}

EchoClient 创建一个套接字,从而获得到 EchoServer 的连接。它在标准输入流上读取用户的输入,然后通过将文本写入套接字来将该文本转发到 EchoServerEchoServer 将输入通过套接字回显到 EchoClientEchoClient 读取并显示从服务器传回的数据。 EchoServer 可以通过虚拟线程同时服务多个客户端,每个客户端连接一个线程。

public class EchoClient {
    public static void main(String[] args) throws IOException {
        if (args.length != 2) {
            System.err.println(
                "Usage: java EchoClient <hostname> <port>");
            System.exit(1);
        }
        String hostName = args[0];
        int portNumber = Integer.parseInt(args[1]);
        try (
            Socket echoSocket = new Socket(hostName, portNumber);
            PrintWriter out =
                new PrintWriter(echoSocket.getOutputStream(), true);
            BufferedReader in =
                new BufferedReader(
                    new InputStreamReader(echoSocket.getInputStream()));
        ) {
            BufferedReader stdIn =
                new BufferedReader(
                    new InputStreamReader(System.in));
            String userInput;
            while ((userInput = stdIn.readLine()) != null) {
                out.println(userInput);
                System.out.println("echo: " + in.readLine());
                if (userInput.equals("bye")) break;
            }
        } catch (UnknownHostException e) {
            System.err.println("Don't know about host " + hostName);
            System.exit(1);
        } catch (IOException e) {
            System.err.println("Couldn't get I/O for the connection to " +
                hostName);
            System.exit(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 操作来避免频繁且长期的固定。