Spring 线程池

最后更新: 2023-01-02 16:27:18 +0800 +0800
原文以英文撰写。您正在阅读AI翻译版本。

Spring 的 TaskExecutor 抽象任务提交。常见的 ThreadPoolTaskExecutor 是围绕 ThreadPoolExecutor 的受管 JavaBean 包装器;其线程池、队列、拒绝和生命周期选择仍决定系统容量。

定义和命名执行器 #

按照工作负载命名执行器,使调用方选择预期资源:

java
@Bean("reportExecutor")
ThreadPoolTaskExecutor reportExecutor() {
    var executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(8);
    executor.setMaxPoolSize(24);
    executor.setQueueCapacity(100);
    executor.setKeepAliveSeconds(30);
    executor.setThreadNamePrefix("report-");
    executor.setRejectedExecutionHandler(
        new ThreadPoolExecutor.CallerRunsPolicy());
    return executor;
}

Spring 会将 ThreadPoolTaskExecutor 作为受管 bean 初始化和关闭。 返回其具体类型可以保留生命周期配置和监控方法。 应使用不同执行器隔离具有不同延迟、阻塞、优先级或下游容量要求的工作负载。

线程池、队列和饱和行为 #

底层执行器遵循明确的接纳顺序:

  1. 创建工作线程,直到达到 corePoolSize。
  2. 工作队列接受任务时,将新任务排队。
  3. 只有队列拒绝任务后,才创建线程,直到达到 maxPoolSize。
  4. 线程池和队列都饱和时,应用拒绝策略。

因此,无界队列会阻止线程数超过核心大小,并将过载转化为不断增长的延迟和内存使用。 有界队列能使过载可观察。AbortPolicy 以异常拒绝,CallerRunsPolicy 通过让提交线程运行任务施加背压, 而丢弃策略会丢失工作,需要明确的产品决策。

核心/最大大小、队列容量和保活时间必须依据测得的任务服务时间、阻塞比例、到达突发和下游限制。 更大的线程池无法增加数据库的连接容量。

@Async 选择和失败 #

使用 @EnableAsync 启用方法拦截。当应用有多个执行器时,选择一个命名执行器:

java
@Async("reportExecutor")
public CompletableFuture<Report> render(ReportRequest request) {
    return CompletableFuture.completedFuture(renderer.render(request));
}

没有注解限定符时,基础设施会查找唯一的 TaskExecutor,或查找名为 taskExecutor 的 Executor bean; AsyncConfigurer 可以提供显式默认值。基于代理的 @Async 与其他 Spring AOP 通知具有相同的自调用限制。

Future 或 CompletableFuture 方法的失败通过其结果观察。 void 异步方法没有结果通道,因此应配置 AsyncUncaughtExceptionHandler; 单独记录日志不是交付或重试保证。

上下文传播 #

执行器任务在另一个线程上运行。线程本地 MDC、安全、区域设置和观测上下文不会自动传递。 TaskDecorator 可以在提交任务时捕获选定状态,在执行前后恢复状态,并在 finally 中清理。 只复制必需上下文,因为过时或秘密状态泄漏到复用的工作线程是正确性和安全缺陷。

优雅关闭 #

关闭策略必须匹配任务语义。配置执行器是否等待排队工作,并设置有限的等待终止时间。 服务关闭期间,先停止接受新请求,然后在截止时间内让已接受工作完成,最后取消或持久化未完成工作。 内存中的执行器队列不是持久存储。

Spring Boot 和虚拟线程 #

Spring Boot 3.2 及更高版本可以使用 Java 21 虚拟线程作为自动配置的异步执行器:

properties
spring.threads.virtual.enabled=true

虚拟线程降低每个阻塞任务占用一个线程的成本,但不会限制对数据库、HTTP 服务或其他稀缺资源的并发调用。 为此应使用信号量、连接池或工作负载接纳限制。 当 Boot 选择按任务创建虚拟线程的执行器时,许多线程池大小属性不适用。

在 JDK 21 到 23 中,持有监视器时阻塞,或在某些本地代码中阻塞,可能将虚拟线程固定到载体线程。 JDK 24 的 JEP 491 消除了基于监视器的固定,但本地和外部函数边界仍需测量。 使用 Java Flight Recorder 和线程转储诊断实际部署的 JDK 版本,不要假设虚拟线程让所有阻塞都无害。

主要参考资料 #

评论

在 GitHub 上阅读或评论(发表评论需要登录)