欢迎光临
我们一直在努力

实战Java虚拟线程:如何用Virtual Threads轻松应对百万级并发

Java虚拟线程实战Java 21正式引入了虚拟线程(Virtual Threads),这是Project Loom项目的核心成果。传统Java应用中,每个请求占用一个平台线程,当并发量飙升时,线程池迅速耗尽,系统响应急剧下降。虚拟线程通过将线程的调度从操作系统内核转移到JVM层面,用极低的内存开销实现了百万级并发连接,彻底改变了Java高并发编程的范式。

本文将通过实际代码示例,带你从零掌握Virtual Threads的核心概念、使用方式、常见陷阱以及性能调优技巧。

一、什么是虚拟线程

Java中的线程一直是对操作系统内核线程的一对一映射。每个平台线程(Platform Thread)默认占用约1MB的栈内存,这意味着一台16GB内存的机器最多只能创建几千个线程。虚拟线程则是由JVM自行调度的轻量级线程,初始栈内存仅几百字节,可以轻松创建数百万个而不耗尽内存。

虚拟线程的关键特性:

  • 极轻量:初始内存占用仅几百字节,按需增长
  • JVM调度:由ForkJoinPool调度,不依赖操作系统线程调度器
  • 挂载/卸载:当虚拟线程执行阻塞操作(I/O、sleep等)时,会自动从载体线程上卸载,释放载体给其他虚拟线程使用

二、创建虚拟线程的几种方式

Java 21提供了多种创建虚拟线程的方式,从最简单的Thread.ofVirtual()到结构化并发,覆盖不同场景。

import java.util.concurrent.*;

public class VirtualThreadDemo {
    public static void main(String[] args) throws Exception {
        // 方式1:直接创建并启动虚拟线程
        Thread vt = Thread.ofVirtual().name("my-vt").start(() -> {
            System.out.println("Running in: " + Thread.currentThread());
        });
        vt.join();

        // 方式2:使用虚拟线程ExecutorService(推荐)
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            executor.submit(() -> {
                System.out.println("Task 1 done");
            });
        }

        // 方式3:批量创建10万个虚拟线程
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 100_000; i++) {
                final int taskId = i;
                executor.submit(() -> {
                    Thread.sleep(Duration.ofMillis(100));
                    return "Task " + taskId;
                });
            }
        }
    }
}

代码编程

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

下面通过一个模拟I/O密集型任务的benchmark,直观对比虚拟线程和传统线程池的表现。

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

public class BenchmarkDemo {
    static String ioTask(int id) throws Exception {
        Thread.sleep(Duration.ofMillis(200));
        return "Result-" + id;
    }

    public static void main(String[] args) throws Exception {
        int taskCount = 10_000;

        // 测试1:传统固定线程池(200线程)
        long start = System.currentTimeMillis();
        try (var pool = Executors.newFixedThreadPool(200)) {
            var futures = new ArrayList>();
            for (int i = 0; i < taskCount; i++) {
                final int id = i;
                futures.add(pool.submit(() -> ioTask(id)));
            }
            for (var f : futures) f.get();
        }
        long platformTime = System.currentTimeMillis() - start;
        System.out.println("Platform Threads: " + platformTime + "ms");

        // 测试2:虚拟线程
        start = System.currentTimeMillis();
        try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
            var futures = new ArrayList>();
            for (int i = 0; i < taskCount; i++) {
                final int id = i;
                futures.add(pool.submit(() -> ioTask(id)));
            }
            for (var f : futures) f.get();
        }
        long virtualTime = System.currentTimeMillis() - start;
        System.out.println("Virtual Threads: " + virtualTime + "ms");
    }
}

在典型的I/O密集型场景中,虚拟线程的吞吐量通常是200线程池的5-10倍,因为10000个任务可以几乎同时开始sleep,而不是排队等待线程。

四、常见陷阱:Pinning问题

虚拟线程并非万能药。当虚拟线程在synchronized块内执行阻塞操作时,会发生线程钉住(Pinning),即虚拟线程无法从载体线程卸载,导致载体线程被长时间占用。

import java.util.concurrent.locks.ReentrantLock;
import java.time.Duration;

public class PinningExample {
    // synchronized + 阻塞 = pinning (应避免)
    private static final Object lock = new Object();
    static void badExample() throws Exception {
        synchronized (lock) {
            Thread.sleep(Duration.ofSeconds(1)); // pinning!
        }
    }

    // ReentrantLock + 阻塞 = 正常卸载 (推荐)
    private static final ReentrantLock reentrantLock = new ReentrantLock();
    static void goodExample() throws Exception {
        reentrantLock.lock();
        try {
            Thread.sleep(Duration.ofSeconds(1)); // 正常卸载
        } finally {
            reentrantLock.unlock();
        }
    }
}

可以通过JVM参数-Djdk.tracePinnedThreads=short来检测pinning问题,开发阶段建议开启。

服务器架构

五、在Spring Boot中使用虚拟线程

Spring Boot 3.2+原生支持虚拟线程,只需一行配置即可让Tomcat使用虚拟线程处理请求。

# application.yml
spring:
  threads:
    virtual:
      enabled: true

启用后,每个HTTP请求都会在一个独立的虚拟线程中处理,即使存在大量慢接口调用,也不会耗尽线程池。对于更早的Spring Boot版本,也可以手动配置Tomcat的线程池:

@Bean
TomatorProtocolHandlerCustomizer virtualThreads() {
    return handler -> handler.setExecutor(
        Executors.newVirtualThreadPerTaskExecutor()
    );
}

总结

Java虚拟线程是近年来Java平台最重要的特性之一,核心要点如下:

  • 适用场景:I/O密集型任务(网络请求、文件读写、数据库查询),而非CPU密集型计算
  • 创建方式:推荐使用Executors.newVirtualThreadPerTaskExecutor(),每个任务一个虚拟线程
  • 避免pinning:用ReentrantLock替代synchronized,避免在锁内执行阻塞操作
  • 框架支持:Spring Boot 3.2+、Quarkus 3.x、Helidon 4.x均已原生支持
  • 监控手段:使用-Djdk.tracePinnedThreads=short检测pinning,JFR事件监控虚拟线程生命周期

虚拟线程让Java开发者可以用最简单的同步代码风格,获得接近异步编程的高并发性能,是构建高吞吐后端服务的利器。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » 实战Java虚拟线程:如何用Virtual Threads轻松应对百万级并发
分享到: 更多 (0)