05、Spring Boot 4 实战:虚拟线程(Virtual Threads)完整实践指南

兄弟们,鹏磊今天来聊聊虚拟线程这玩意儿;这是 Java 21 最重要的特性之一,也是 Spring Boot 4 深度集成的功能。虚拟线程能支持百万级并发,性能提升不是一点半点;如果你要做高并发应用,这玩意儿绝对值得研究。这功能确实牛逼,用好了性能提升很明显。

一、什么是虚拟线程?

1. 基本概念

虚拟线程(Virtual Threads)是 Java 21 引入的轻量级线程,由 JVM 管理,不是操作系统线程;一个平台线程(Platform Thread)可以运行成千上万个虚拟线程。这玩意儿比传统线程轻量多了,创建和销毁的开销很小,能支持百万级并发。

// 传统平台线程,一个线程对应一个操作系统线程
Thread platformThread = Thread.ofPlatform()
    .name("platform-thread")
    .start(() -> {
        System.out.println("这是平台线程: " + Thread.currentThread().getName());
    });

// 虚拟线程,多个虚拟线程可以共享一个平台线程
Thread virtualThread = Thread.ofVirtual()
    .name("virtual-thread")
    .start(() -> {
        System.out.println("这是虚拟线程: " + Thread.currentThread().getName());
        System.out.println("是否是虚拟线程: " + Thread.currentThread().isVirtual());
    });

// 输出:
// 这是平台线程: platform-thread
// 这是虚拟线程: virtual-thread
// 是否是虚拟线程: true

2. 虚拟线程 vs 平台线程

虚拟线程和平台线程的主要区别:

| 特性 | 平台线程 | 虚拟线程 | |------|---------|---------| | 资源消耗 | 高(每个线程占用 1MB+ 内存) | 低(每个线程占用几KB内存) | | 创建开销 | 高(需要系统调用) | 低(JVM 内部管理) | | 数量限制 | 受操作系统限制(通常几千个) | 几乎无限制(可创建百万个) | | 适用场景 | CPU 密集型任务 | I/O 密集型任务 | | 阻塞成本 | 高(占用操作系统线程) | 低(自动挂起,不占线程) |

// 对比创建线程的开销
public class ThreadComparison {
    
    public static void main(String[] args) throws InterruptedException {
        // 创建 10000 个平台线程,会占用大量资源
        long start = System.currentTimeMillis();
        List<Thread> platformThreads = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            Thread thread = Thread.ofPlatform()
                .start(() -> {
                    try {
                        Thread.sleep(100);  // 模拟 I/O 操作
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            platformThreads.add(thread);
        }
        // 等待所有线程完成
        for (Thread thread : platformThreads) {
            thread.join();
        }
        long platformTime = System.currentTimeMillis() - start;
        
        // 创建 10000 个虚拟线程,资源占用很小
        start = System.currentTimeMillis();
        List<Thread> virtualThreads = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            Thread thread = Thread.ofVirtual()
                .start(() -> {
                    try {
                        Thread.sleep(100);  // 模拟 I/O 操作,虚拟线程会自动挂起
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            virtualThreads.add(thread);
        }
        // 等待所有线程完成
        for (Thread thread : virtualThreads) {
            thread.join();
        }
        long virtualTime = System.currentTimeMillis() - start;
        
        System.out.println("平台线程耗时: " + platformTime + "ms");
        System.out.println("虚拟线程耗时: " + virtualTime + "ms");
        // 虚拟线程通常更快,因为创建和调度开销小
    }
}

二、为什么需要虚拟线程?

1. 传统线程的局限性

传统线程模型有几个问题:

  • 资源消耗大:每个线程占用 1MB+ 内存,创建大量线程会耗尽内存
  • 创建开销高:创建线程需要系统调用,开销大
  • 数量限制:受操作系统限制,通常只能创建几千个线程
  • 阻塞成本高:线程阻塞时占用操作系统线程,资源浪费
// 传统方式处理大量并发请求,受线程数量限制
@Service
public class TraditionalUserService {
    
    private final ExecutorService executor = Executors.newFixedThreadPool(200);  // 最多 200 个线程
    
    public CompletableFuture<User> fetchUser(Long id) {
        // 如果并发请求超过 200 个,后面的请求会排队等待
        // 线程池满了,性能会急剧下降
        return CompletableFuture.supplyAsync(() -> {
            // 模拟数据库查询,会阻塞线程
            return userRepository.findById(id);
        }, executor);
    }
}

2. 虚拟线程的优势

虚拟线程解决了这些问题:

  • 资源消耗小:每个虚拟线程只占用几KB内存,可以创建百万个
  • 创建开销低:JVM 内部管理,不需要系统调用
  • 数量无限制:可以创建百万甚至千万个虚拟线程
  • 阻塞成本低:虚拟线程阻塞时自动挂起,不占用平台线程
// 使用虚拟线程处理大量并发请求,不受线程数量限制
@Service
public class VirtualThreadUserService {
    
    private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();  // 虚拟线程执行器
    
    public CompletableFuture<User> fetchUser(Long id) {
        // 可以创建百万个虚拟线程,不受限制
        // 每个请求都有独立的虚拟线程,不会排队
        return CompletableFuture.supplyAsync(() -> {
            // 模拟数据库查询,虚拟线程会自动挂起,不占用平台线程
            return userRepository.findById(id);
        }, executor);
    }
}

三、如何创建虚拟线程?

1. 使用 Thread.ofVirtual()

最简单的方式是用 Thread.ofVirtual() 创建虚拟线程:

// 方式1:直接创建并启动虚拟线程
Thread virtualThread = Thread.ofVirtual()
    .name("my-virtual-thread")  // 设置线程名称
    .start(() -> {
        // 任务逻辑
        System.out.println("虚拟线程执行任务");
    });

// 方式2:创建未启动的虚拟线程
Thread virtualThread = Thread.ofVirtual()
    .name("my-virtual-thread")
    .unstarted(() -> {
        System.out.println("虚拟线程执行任务");
    });
virtualThread.start();  // 手动启动

// 方式3:创建虚拟线程工厂
ThreadFactory virtualThreadFactory = Thread.ofVirtual()
    .name("worker-", 0)  // 名称前缀和起始编号
    .factory();

Thread thread1 = virtualThreadFactory.newThread(() -> System.out.println("任务1"));
Thread thread2 = virtualThreadFactory.newThread(() -> System.out.println("任务2"));
thread1.start();
thread2.start();

2. 使用 Executors.newVirtualThreadPerTaskExecutor()

推荐用 Executors.newVirtualThreadPerTaskExecutor() 创建虚拟线程执行器:

// 创建虚拟线程执行器,每个任务都会创建一个新的虚拟线程
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

// 提交任务,每个任务都在独立的虚拟线程上运行
for (int i = 0; i < 1000000; i++) {
    int taskId = i;
    executor.submit(() -> {
        // 处理任务,可以创建百万个虚拟线程,不受限制
        processTask(taskId);
    });
}

// 关闭执行器
executor.shutdown();
try {
    executor.awaitTermination(1, TimeUnit.HOURS);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

3. 使用虚拟线程执行器服务

虚拟线程执行器服务可以替代传统的线程池:

@Service
public class TaskService {
    
    // 使用虚拟线程执行器,替代传统线程池
    private final ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();
    
    public void processTasks(List<Task> tasks) {
        // 为每个任务创建虚拟线程,不受线程数量限制
        List<CompletableFuture<Void>> futures = tasks.stream()
            .map(task -> CompletableFuture.runAsync(() -> {
                // 处理任务,虚拟线程会自动管理
                processTask(task);
            }, virtualExecutor))
            .toList();
        
        // 等待所有任务完成
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
    }
    
    private void processTask(Task task) {
        // 任务处理逻辑,I/O 操作时虚拟线程会自动挂起
        try {
            Thread.sleep(100);  // 模拟 I/O 操作
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

四、Spring Boot 4 中的虚拟线程

1. 启用虚拟线程

在 Spring Boot 4 中,可以通过配置启用虚拟线程:

# application.yml 配置
spring:
  threads:
    virtual:
      enabled: true  # 启用虚拟线程,只有 Java 21 才支持

2. 配置虚拟线程执行器

在代码里配置虚拟线程执行器:

@Configuration
public class VirtualThreadConfig {
    
    @Bean
    public Executor virtualThreadExecutor() {
        // 创建虚拟线程执行器,底层用的是 Java 21 的虚拟线程
        // 这玩意儿比传统线程池强多了,性能提升不是一点半点
        return Executors.newVirtualThreadPerTaskExecutor();
    }
    
    // 配置异步任务使用虚拟线程
    @Bean
    public TaskExecutor taskExecutor() {
        // 返回虚拟线程执行器,@Async 注解会使用这个执行器
        return new TaskExecutor() {
            private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
            
            @Override
            public void execute(Runnable task) {
                executor.submit(task);
            }
        };
    }
}

3. 配置 WebMvc 使用虚拟线程

让 Tomcat 使用虚拟线程处理请求:

@Configuration
public class WebMvcVirtualThreadConfig {
    
    // 配置 Tomcat 使用虚拟线程处理请求
    @Bean
    public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
        // 让 Tomcat 使用虚拟线程处理请求,性能提升明显
        // 每个 HTTP 请求都在独立的虚拟线程上处理,不受线程数量限制
        return protocolHandler -> {
            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
        };
    }
}

4. 使用 @Async 注解

@Async 注解也可以使用虚拟线程:

@Service
public class AsyncService {
    
    @Async  // 使用虚拟线程执行异步任务
    public CompletableFuture<String> asyncTask(String input) {
        // 异步任务逻辑,在虚拟线程上执行
        try {
            Thread.sleep(1000);  // 模拟耗时操作,虚拟线程会自动挂起
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return CompletableFuture.completedFuture("处理完成: " + input);
    }
}

@RestController
public class AsyncController {
    
    @Autowired
    private AsyncService asyncService;
    
    @GetMapping("/async")
    public CompletableFuture<String> handleAsync() {
        // 调用异步方法,在虚拟线程上执行
        return asyncService.asyncTask("请求数据");
    }
}

五、虚拟线程的最佳实践

1. 适合的场景

虚拟线程适合以下场景:

  • I/O 密集型任务:数据库查询、HTTP 请求、文件读写等
  • 高并发应用:需要处理大量并发请求的 Web 应用
  • 微服务调用:服务间调用,大部分时间在等待响应
// 适合使用虚拟线程的场景:I/O 密集型任务
@Service
public class IoIntensiveService {
    
    private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
    
    public CompletableFuture<String> fetchData(String url) {
        // HTTP 请求,I/O 操作,适合用虚拟线程
        return CompletableFuture.supplyAsync(() -> {
            // 虚拟线程在等待 HTTP 响应时会自动挂起,不占用平台线程
            return httpClient.get(url);
        }, executor);
    }
    
    public CompletableFuture<User> queryUser(Long id) {
        // 数据库查询,I/O 操作,适合用虚拟线程
        return CompletableFuture.supplyAsync(() -> {
            // 虚拟线程在等待数据库响应时会自动挂起
            return userRepository.findById(id);
        }, executor);
    }
}

2. 不适合的场景

虚拟线程不适合以下场景:

  • CPU 密集型任务:大量计算,没有 I/O 操作
  • 长时间运行的同步代码:会占用平台线程
// 不适合使用虚拟线程的场景:CPU 密集型任务
@Service
public class CpuIntensiveService {
    
    // CPU 密集型任务,不适合用虚拟线程
    // 应该用平台线程或者 ForkJoinPool
    public void heavyComputation() {
        // 大量计算,没有 I/O 操作,虚拟线程不会带来性能提升
        long result = 0;
        for (int i = 0; i < 1000000000; i++) {
            result += i * i;  // CPU 密集型计算
        }
    }
    
    // 适合用 ForkJoinPool 处理 CPU 密集型任务
    private final ForkJoinPool forkJoinPool = new ForkJoinPool();
    
    public void parallelComputation() {
        forkJoinPool.submit(() -> {
            // CPU 密集型任务,用 ForkJoinPool 更合适
            heavyComputation();
        });
    }
}

3. 避免固定虚拟线程

虚拟线程阻塞时会自动挂起,所以不要用 synchronized 等会固定虚拟线程的操作:

// 错误示例:使用 synchronized 会固定虚拟线程
public class BadExample {
    private final Object lock = new Object();
    
    public void badMethod() {
        synchronized (lock) {  // synchronized 会固定虚拟线程到平台线程
            // 如果这里阻塞,虚拟线程会被固定到平台线程,失去优势
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

// 正确示例:使用 ReentrantLock 替代 synchronized
public class GoodExample {
    private final ReentrantLock lock = new ReentrantLock();
    
    public void goodMethod() {
        lock.lock();  // ReentrantLock 不会固定虚拟线程
        try {
            // 虚拟线程可以正常挂起和恢复
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        } finally {
            lock.unlock();
        }
    }
}

4. 合理使用虚拟线程执行器

不要为每个任务都创建新的虚拟线程执行器,应该复用:

// 错误示例:为每个任务创建新的执行器
public class BadExecutorUsage {
    public void badMethod() {
        // 每次都创建新的执行器,浪费资源
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
        executor.submit(() -> System.out.println("任务"));
        executor.shutdown();  // 立即关闭,任务可能还没执行完
    }
}

// 正确示例:复用执行器
@Service
public class GoodExecutorUsage {
    // 在类级别创建执行器,复用
    private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
    
    public void goodMethod() {
        // 复用执行器,资源利用更高效
        executor.submit(() -> System.out.println("任务"));
    }
    
    @PreDestroy
    public void cleanup() {
        // 应用关闭时关闭执行器
        executor.shutdown();
    }
}

六、虚拟线程的性能优化

1. 系统属性配置

虚拟线程调度器可以通过系统属性配置:

# 设置虚拟线程调度器的并行度,默认是 CPU 核心数
-Djdk.virtualThreadScheduler.parallelism=8

# 设置虚拟线程调度器的最大线程池大小,默认是 256
-Djdk.virtualThreadScheduler.maxPoolSize=512

# 运行应用
java -Djdk.virtualThreadScheduler.parallelism=8 \
     -Djdk.virtualThreadScheduler.maxPoolSize=512 \
     -jar myapp.jar

2. 监控虚拟线程

可以通过 JVM 工具监控虚拟线程:

// 监控虚拟线程数量
public class VirtualThreadMonitor {
    
    public void monitorVirtualThreads() {
        ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
        
        // 获取所有线程
        ThreadInfo[] threads = threadBean.dumpAllThreads(false, false);
        
        // 统计虚拟线程数量
        long virtualThreadCount = Arrays.stream(threads)
            .filter(thread -> {
                // 检查是否是虚拟线程
                // 注意:ThreadInfo 可能不直接支持 isVirtual(),需要其他方式判断
                return thread.getThreadName().contains("VirtualThread");
            })
            .count();
        
        System.out.println("虚拟线程数量: " + virtualThreadCount);
    }
}

七、虚拟线程的限制和注意事项

1. 固定虚拟线程的操作

以下操作会固定虚拟线程到平台线程:

  • synchronized 关键字
  • Object.wait()Object.notify()
  • Thread.sleep() 不会固定,但某些 JNI 调用会固定
// 会固定虚拟线程的操作
public class PinningExample {
    
    private final Object lock = new Object();
    
    public void pinnedMethod() {
        synchronized (lock) {  // synchronized 会固定虚拟线程
            // 虚拟线程被固定到平台线程,失去优势
        }
    }
    
    // 使用 ReentrantLock 替代 synchronized
    private final ReentrantLock reentrantLock = new ReentrantLock();
    
    public void unpinnedMethod() {
        reentrantLock.lock();  // ReentrantLock 不会固定虚拟线程
        try {
            // 虚拟线程可以正常挂起和恢复
        } finally {
            reentrantLock.unlock();
        }
    }
}

2. 线程本地变量

虚拟线程支持线程本地变量,但要注意内存泄漏:

// 线程本地变量在虚拟线程中的使用
public class ThreadLocalExample {
    
    private static final ThreadLocal<String> context = new ThreadLocal<>();
    
    public void useThreadLocal() {
        // 虚拟线程支持 ThreadLocal
        context.set("虚拟线程上下文");
        
        // 使用完后要清理,避免内存泄漏
        try {
            String value = context.get();
            System.out.println("上下文值: " + value);
        } finally {
            context.remove();  // 清理线程本地变量
        }
    }
}

3. 调试虚拟线程

虚拟线程的调试和传统线程类似,但要注意线程名称:

// 设置虚拟线程名称,方便调试
Thread virtualThread = Thread.ofVirtual()
    .name("worker-", 0)  // 设置名称前缀和编号
    .start(() -> {
        // 任务逻辑
        System.out.println("线程名称: " + Thread.currentThread().getName());
    });

八、性能对比

1. 并发性能对比

虚拟线程在高并发场景下性能提升明显:

// 性能对比测试
public class PerformanceComparison {
    
    public static void main(String[] args) throws InterruptedException {
        int taskCount = 100000;  // 10 万个任务
        
        // 测试平台线程池
        long start = System.currentTimeMillis();
        ExecutorService platformExecutor = Executors.newFixedThreadPool(200);
        for (int i = 0; i < taskCount; i++) {
            platformExecutor.submit(() -> {
                try {
                    Thread.sleep(100);  // 模拟 I/O 操作
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }
        platformExecutor.shutdown();
        platformExecutor.awaitTermination(1, TimeUnit.HOURS);
        long platformTime = System.currentTimeMillis() - start;
        
        // 测试虚拟线程
        start = System.currentTimeMillis();
        ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();
        for (int i = 0; i < taskCount; i++) {
            virtualExecutor.submit(() -> {
                try {
                    Thread.sleep(100);  // 模拟 I/O 操作
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }
        virtualExecutor.shutdown();
        virtualExecutor.awaitTermination(1, TimeUnit.HOURS);
        long virtualTime = System.currentTimeMillis() - start;
        
        System.out.println("平台线程耗时: " + platformTime + "ms");
        System.out.println("虚拟线程耗时: " + virtualTime + "ms");
        System.out.println("性能提升: " + (platformTime - virtualTime) * 100.0 / platformTime + "%");
        // 虚拟线程通常能提升 30-50% 的性能
    }
}

九、总结

虚拟线程是 Java 21 最重要的特性之一,也是 Spring Boot 4 深度集成的功能。主要优势:

  • 资源消耗小:可以创建百万个虚拟线程,这玩意儿确实轻量
  • 性能提升明显:I/O 密集型任务性能提升 30-50%,这提升不是一点半点
  • 使用简单:和传统线程 API 兼容,学习成本低,上手快

反正鹏磊觉得这功能确实实用,特别是做高并发应用的时候,性能提升很明显。

适用场景:

  • I/O 密集型任务:数据库查询、HTTP 请求等
  • 高并发应用:需要处理大量并发请求的 Web 应用
  • 微服务调用:服务间调用,大部分时间在等待响应

注意事项:

  • 不适合 CPU 密集型任务:应该用平台线程或 ForkJoinPool
  • 避免使用 synchronized:会固定虚拟线程,失去优势
  • 合理使用执行器:复用执行器,不要为每个任务创建新的

好了,今天就聊到这;下一篇咱详细说说声明式 HTTP 客户端 @HttpExchange 从入门到精通,包括怎么用声明式客户端简化 HTTP 调用。兄弟们有啥问题可以在评论区留言,鹏磊看到会回复的。

本文章最后更新于 2025-11-27