Java 虚拟线程(Virtual Threads)实战

在 Java 长期发展历程中,线程模型几乎没有颠覆性变化——直到虚拟线程(Virtual Threads)的出现。作为 Java 19 引入的预览特性(JEP 425)并在 Java 21 正式转正,虚拟线程彻底改变了我们处理并发任务的方式。本文将从实战角度出发,带你理解虚拟线程的原理、使用场景,并通过代码示例展示其如何轻松支撑百万级并发。

一、虚拟线程是什么?—— 打破传统线程的桎梏

传统线程的困境

Java 中的 Thread 长期以来都是平台线程(Platform Thread) 的封装,它与操作系统内核线程一一对应。这种模型有两个致命缺陷:

  1. 资源昂贵:每个平台线程会占用 1-2MB 栈内存,创建数千个线程就可能耗尽系统资源。

  2. 阻塞浪费:当线程因 IO 操作(如数据库查询、网络请求)阻塞时,对应的内核线程也会闲置,造成资源浪费。

这就是为什么我们需要线程池来限制线程数量,但线程池本质上是一种妥协——它通过限制并发量来避免资源耗尽,却也限制了系统的吞吐量。

虚拟线程的突破

虚拟线程是 Java 虚拟机(JVM)管理的用户态线程,它与内核线程不再是一一对应关系:

  • 轻量级:单个虚拟线程的栈内存可以动态伸缩(从几 KB 开始),理论上可创建数百万个。

  • M:N 调度:多个虚拟线程映射到少量平台线程上运行,当虚拟线程阻塞时,JVM 会将其挂载(unpark),释放平台线程供其他虚拟线程使用。

简单说,虚拟线程让我们可以像创建对象一样轻松创建线程,彻底摆脱线程数量的束缚。

二、快速上手:3 种创建虚拟线程的方式

虚拟线程的 API 设计非常友好,与传统线程兼容,学习成本极低。以下是最常用的创建方式:

方式 1:通过 Thread.startVirtualThread() 创建

这是创建单个虚拟线程最直接的方式:

        // 创建并启动虚拟线程
        Thread virtualThread = Thread.startVirtualThread(() -> {
            System.out.println("虚拟线程运行中...");
            System.out.println("线程类型: " + Thread.currentThread().getClass().getSimpleName());
        });

        // 等待虚拟线程完成
        virtualThread.join();

输出结果:

虚拟线程运行中...
线程类型: VirtualThread

方式 2:通过 Thread.Builder 定制虚拟线程

如需设置线程名、异常处理器等属性,可使用构建器模式:

        Thread virtualThread = Thread.ofVirtual()
                .name("order-process-1") // 设置线程名
                .uncaughtExceptionHandler((t, e) -> {
                    System.err.println("线程 " + t.getName() + " 发生异常: " + e.getMessage());
                })
                .start(() -> {
                    System.out.println("处理订单...");
                    // 模拟业务逻辑(如数据库查询、网络请求等IO操作)
                    try {
                        Thread.sleep(Duration.ofSeconds(5));
                        System.out.println("任务执行完成");
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        System.out.println("任务被中断");
                    }
                });

        virtualThread.join();

方式 3:通过 ExecutorService 批量管理虚拟线程

在实际开发中,我们通常需要管理多个虚拟线程,此时可使用

        // 创建虚拟线程池(每个任务对应一个虚拟线程)
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            // 提交 1000 个任务
            for (int i = 0; i < 1000; i++) {
                int taskId = i;
                executor.submit(() -> {
                    System.out.println("执行任务 " + taskId + ",线程: " + Thread.currentThread().getName());
                    // 模拟 IO 操作
                    Thread.sleep(Duration.ofMillis(100));
                    return taskId;
                });
            }
        }catch (Exception e) {
            e.printStackTrace();
        }

这个例子中,我们轻松创建了 1000 个并发任务,而系统资源几乎没有压力——这在传统线程模型中是难以想象的。

三、性能对比:虚拟线程 vs 平台线程

为了直观感受虚拟线程的优势,我们设计一个简单的性能测试:模拟高并发 IO 场景(每个任务包含 100ms 的模拟网络请求),对比虚拟线程与平台线程的吞吐量。

测试代码

package com.sucl.virtualthread.test;

import org.junit.jupiter.api.Test;

import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ThreadPerformanceTest {
    // 测试任务数量
    private static final int TASK_COUNT = 10_000;
    // 每个任务的模拟 IO 等待时间
    private static final Duration IO_WAIT = Duration.ofMillis(100);

    @Test
    public void test() throws InterruptedException {
        // 测试平台线程池(核心线程数 100)
        testPlatformThreads();
        // 测试虚拟线程池
        testVirtualThreads();
    }

    private static void testPlatformThreads() throws InterruptedException {
        long start = System.currentTimeMillis();
        // 创建固定大小的平台线程池
        try (ExecutorService executor = Executors.newFixedThreadPool(100)) {
            for (int i = 0; i < TASK_COUNT; i++) {
                executor.submit(() -> {
                    try {
                        // 模拟 IO 操作(如数据库查询、网络请求)
                        Thread.sleep(IO_WAIT);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            }
        }
        long end = System.currentTimeMillis();
        System.out.printf("平台线程完成 %d 个任务,耗时: %d ms%n", 
                         TASK_COUNT, end - start);
    }

    private static void testVirtualThreads() throws InterruptedException {
        long start = System.currentTimeMillis();
        // 创建虚拟线程池
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < TASK_COUNT; i++) {
                executor.submit(() -> {
                    try {
                        // 模拟 IO 操作
                        Thread.sleep(IO_WAIT);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            }
        }
        long end = System.currentTimeMillis();
        System.out.printf("虚拟线程完成 %d 个任务,耗时: %d ms%n", 
                         TASK_COUNT, end - start);
    }
}

测试结果(JDK 21,8 核 CPU)

平台线程完成 10000 个任务,耗时: 10912 ms
虚拟线程完成 10000 个任务,耗时: 456 ms

结果分析

  • 平台线程池因受限于 100 个核心线程,需要分 100 批执行(10000/100),总耗时约为 100 * 100ms = 10 秒。

  • 虚拟线程几乎可以同时执行所有任务,总耗时仅略大于单个任务的 IO 等待时间(100ms),吞吐量提升近 8 倍!

四、实战场景:虚拟线程在 Web 服务中的应用

虚拟线程最适合 IO 密集型场景,如 Web 服务、微服务调用、数据库操作等。下面以 Spring Boot 应用为例,展示如何用虚拟线程提升接口吞吐量。

步骤 1:升级 Spring Boot 版本

Spring Boot 3.2+ 对虚拟线程提供了原生支持,在 pom.xml 中配置:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
</parent>

步骤 2:启用虚拟线程

在 application.properties 中配置,让 Tomcat 使用虚拟线程处理请求:

# 启用虚拟线程
spring.threads.virtual.enabled=true
# Tomcat 连接器使用虚拟线程
server.tomcat.threads.virtual.enabled=true

步骤 3:编写控制器测试

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.Duration;
import java.util.concurrent.CompletableFuture;

@RestController
public class VirtualThreadController {

    // 模拟数据库查询的 IO 操作
    private String fetchDataFromDb() throws InterruptedException {
        // 模拟 100ms 的数据库查询
        Thread.sleep(Duration.ofMillis(100));
        return "数据库查询结果";
    }

    // 使用虚拟线程处理请求(Spring 会自动分配)
    @GetMapping("/api/data")
    public CompletableFuture<String> getData() {
        // 用 CompletableFuture 包装异步操作
        return CompletableFuture.supplyAsync(() -> {
            try {
                return fetchDataFromDb();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return "查询中断";
            }
        });
    }
}

性能测试(使用 JMeter 模拟 10000 并发请求)

线程模型

平均响应时间

吞吐量(QPS)

错误率

平台线程

876ms

1140

3.2%

虚拟线程

123ms

8130

0%

结论:在 Web 服务场景中,虚拟线程能显著降低响应时间并提升吞吐量,尤其在高并发下优势明显。

五、避坑指南:虚拟线程的使用注意事项

虽然虚拟线程强大,但并非银弹,使用时需注意以下几点:

  1. 不适合 CPU 密集型任务
    虚拟线程的优势在于处理 IO 阻塞,对于纯计算任务(如大数据量排序),其性能与平台线程无异,甚至因调度开销略差。

  2. 避免同步代码块滥用
    虚拟线程数量极多,若大量虚拟线程争夺同一把锁,会导致频繁的上下文切换。建议使用 ReentrantLock 替代 synchronized,或采用无锁设计。

  3. 注意线程局部变量(ThreadLocal)
    虚拟线程会继承平台线程的 ThreadLocal,若 ThreadLocal 中存储大对象,可能导致内存泄漏。建议使用 InheritableThreadLocal 或框架提供的上下文传递机制(如 Spring 的 RequestContextHolder)。

  4. 不要设置过大的栈大小
    虚拟线程默认栈大小动态伸缩,手动设置 -Xss 可能适得其反,浪费资源。

  5. 兼容问题排查
    部分老旧库(如某些 JDBC 驱动、日志框架)可能依赖平台线程的特性,使用前需测试兼容性。可通过 Thread.currentThread().isVirtual() 区分线程类型,针对性处理。

六、总结:虚拟线程带来的变革

虚拟线程不是对传统线程的小修小补,而是 Java 并发模型的一次革命性升级。它让开发者可以用简单的编程模型实现高并发,无需再为线程池参数调优、线程数量限制而烦恼。

  • 对于开发者:可以更专注于业务逻辑,用“一个任务一个线程”的直观方式编写代码。

  • 对于系统:能更高效地利用硬件资源,尤其在云原生环境中,可显著降低资源成本。

虚拟线程的普及可能需要时间,但现在正是学习和实践的最佳时机。不妨从一个简单的 IO 密集型任务开始,体验百万级并发的丝滑感受!

更多推荐