Spring Boot 3虚拟线程高并发处理与协程调度机制实战

Java 21正式引入虚拟线程(Virtual Thread),Spring Boot 3.2+提供开箱即用的虚拟线程支持。虚拟线程由JVM调度而非操作系统,创建和切换成本远低于平台线程。本文通过代码实例分析虚拟线程在Spring Boot中的配置方法和高并发场景下的性能表现。

Java虚拟线程与平台线程的调度差异

传统Java线程映射到操作系统线程(1:1模型),每个线程占用约1MB栈空间,创建和上下文切换需要内核态操作。虚拟线程采用M:N调度模型——多个虚拟线程复用少量载体线程(carrier thread),由JVM在用户态完成调度切换。

// 虚拟线程基础创建方式
// 方式1:直接创建
Thread vt = Thread.ofVirtual().start(() -> {
    System.out.println("Running on virtual thread: " + Thread.currentThread());
});

// 方式2:通过ExecutorService
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    // 提交10000个任务,每个任务一个虚拟线程
    IntStream.range(0, 10000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
} // try-with-resources自动等待所有任务完成

上面代码创建10000个虚拟线程每个休眠1秒,总执行时间约1秒。同样的任务用平台线程池(Executors.newFixedThreadPool(200))需要约50秒。虚拟线程的I/O等待不会阻塞载体线程——当虚拟线程执行阻塞操作时JVM自动卸载(unmount)该虚拟线程,释放载体线程执行其他虚拟线程任务。

Spring Boot 3虚拟线程配置与Tomcat集成

Spring Boot 3.2+只需一行配置启用虚拟线程,Tomcat将为每个HTTP请求分配一个虚拟线程而非从线程池获取:

# application.yml
spring:
  threads:
    virtual:
      enabled: true
  # Tomcat虚拟线程模式下的连接配置
  tomcat:
    threads:
      max: 200          # 载体线程数(ForkJoinPool大小)
    max-connections: 8192
    accept-count: 100
    
# Java版本要求
# pom.xml
<properties>
    <java.version>21</java.version>
</properties>

启用后验证效果——定义一个阻塞I/O的Controller:

@RestController
@RequestMapping("/api")
public class DataController {

    private final ExternalApiService apiService;
    private final UserRepository userRepository;

    public DataController(ExternalApiService apiService, 
                          UserRepository userRepository) {
        this.apiService = apiService;
        this.userRepository = userRepository;
    }

    @GetMapping("/users/{id}")
    public UserDto getUser(@PathVariable Long id) {
        // 虚拟线程模式下,这些阻塞调用不会占用载体线程
        User user = userRepository.findById(id)
            .orElseThrow(() -> new NotFoundException("User not found"));
        
        // 外部API调用(阻塞300ms)
        UserProfile profile = apiService.fetchProfile(user.getExternalId());
        
        // 另一个数据库查询(阻塞50ms)
        List<Order> orders = userRepository.findOrdersByUserId(id);
        
        return new UserDto(user, profile, orders);
    }

    @PostMapping("/batch")
    public List<UserDto> batchGetUsers(@RequestBody List<Long> ids) {
        // 使用StructuredTaskScope并行获取用户数据
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            List<StructuredTaskScope.Subtask<UserDto>> subtasks = ids.stream()
                .map(id -> scope.fork(() -> getUser(id)))
                .toList();
            
            scope.join();           // 等待所有子任务完成
            scope.throwIfFailed();  // 任一失败则抛出异常
            
            return subtasks.stream()
                .map(StructuredTaskScope.Subtask::get)
                .toList();
        }
    }
}

@Async异步方法与虚拟线程的协作模式

启用虚拟线程后,@Async方法也使用虚拟线程执行。配合ThreadPoolTaskExecutor的虚拟线程配置:

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean("virtualThreadExecutor")
    public AsyncTaskExecutor virtualThreadExecutor() {
        // Spring 6.1+ 提供的虚拟线程执行器
        return new TaskExecutorAdapter(
            Executors.newVirtualThreadPerTaskExecutor()
        );
    }
}

@Service
public class NotificationService {

    private final EmailSender emailSender;
    private final SmsSender smsSender;

    @Async("virtualThreadExecutor")
    public CompletableFuture<Void> sendNotification(User user, String message) {
        // 邮件和短信并行发送,各自占用独立虚拟线程
        CompletableFuture<Void> emailFuture = CompletableFuture.runAsync(
            () -> emailSender.send(user.getEmail(), message)
        );
        CompletableFuture<Void> smsFuture = CompletableFuture.runAsync(
            () -> smsSender.send(user.getPhone(), message)
        );
        
        return CompletableFuture.allOf(emailFuture, smsFuture);
    }
}

虚拟线程性能基准测试与踩坑指南

使用wrk对/user/{id}接口做压力测试,对比平台线程和虚拟线程表现:

# 平台线程模式(线程池200)
wrk -t8 -c2000 -d30s http://localhost:8080/api/users/1
# 结果:QPS 1,820,平均延迟 1.08s,P99 3.2s

# 虚拟线程模式
wrk -t8 -c2000 -d30s http://localhost:8080/api/users/1
# 结果:QPS 5,840,平均延迟 0.34s,P99 0.82s

2000并发连接下虚拟线程模式QPS提升3.2倍。原因是平台线程池(200个线程)在2000并发时大量请求排队,而虚拟线程为每个请求创建独立线程,I/O等待期间载体线程可处理其他请求。

虚拟线程有一个核心限制:synchronized代码块内执行的阻塞操作不会卸载虚拟线程。在synchronized块中调用阻塞I/O会导致载体线程被钉住(pinning),失去虚拟线程的并发优势。解决方案是使用ReentrantLock替代synchronized:

// 有问题的代码 - synchronized会导致线程pinning
public synchronized User getFromCache(String key) {
    User cached = cacheMap.get(key);
    if (cached == null) {
        cached = fetchFromDatabase(key);  // 阻塞操作在synchronized内 - pinning!
        cacheMap.put(key, cached);
    }
    return cached;
}

// 修复方案 - 使用ReentrantLock
private final ReentrantLock lock = new ReentrantLock();

public User getFromCache(String key) {
    lock.lock();
    try {
        User cached = cacheMap.get(key);
        if (cached == null) {
            cached = fetchFromDatabase(key);  // 阻塞操作不pinning
            cacheMap.put(key, cached);
        }
        return cached;
    } finally {
        lock.unlock();
    }
}

检测pinning问题在JVM启动参数加-Djdk.tracePinnedThreads=full,运行时控制台会打印pinning发生的位置和栈信息。生产环境推荐切换ReentrantLock并移除synchronized,这是迁移虚拟线程最常见的代码重构工作。ThreadLocal在虚拟线程中仍可用但需注意内存——虚拟线程数量可能达到百万级,ThreadLocal变量会随虚拟线程生命周期持有,建议使用Scoped Values替代。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/springboot3-xu-ni-xian-cheng-gao-bing-fa-chu-li-yu-xie/

(0)
小编小编
上一篇 12小时前
下一篇 12小时前

相关推荐