SpringBoot JDK17 线程池的使用

Spring Boot 3 + JDK 17:线程池创建与使用完全指南

环境基线:JDK 17Spring Boot 3.x (对应 Spring Framework 6.x)。

Spring Boot 3.0 起最低要求就是 Java 17,本文所有代码均可直接在该环境运行。


一、为什么必须用线程池

直接 new Thread() 的问题人尽皆知:线程创建/销毁开销大、无法控制并发总量、出问题难以排查。线程池的价值在于三点:

  1. 复用:核心线程常驻,避免频繁创建销毁;
  2. 管控:用有界队列和拒绝策略给系统加一道保险,防止资源被无限吃光;
  3. 可观测:池本身暴露队列深度、活跃线程数等指标,方便监控和调优。

在 Spring Boot 3 项目里,创建线程池主要有三条路线:

路线 适用场景
JDK 原生 ThreadPoolExecutor 需要完全掌控每个参数、或脱离 Spring 容器的组件
Spring ThreadPoolTaskExecutor + @Async 业务异步方法、需要和 Spring 生命周期/上下文集成
Spring Boot 自动配置属性 快速配置全局默认异步线程池

下面逐一展开。


二、先搞懂 ThreadPoolExecutor 的 7 个参数

无论哪条路线,最终干活的都是 java.util.concurrent.ThreadPoolExecutor,它的构造函数是理解一切线程池的基础:

java 复制代码
public ThreadPoolExecutor(
    int corePoolSize,            // 1. 核心线程数:常驻线程(允许核心线程超时回收除外)
    int maximumPoolSize,         // 2. 最大线程数:队列满后线程数可膨胀到的上限
    long keepAliveTime,          // 3. 非核心线程空闲存活时间
    TimeUnit unit,               // 4. 存活时间单位
    BlockingQueue<Runnable> workQueue,   // 5. 任务等待队列
    ThreadFactory threadFactory,         // 6. 线程工厂(强烈建议自定义命名)
    RejectedExecutionHandler handler)    // 7. 拒绝策略

任务提交的流转顺序(面试高频,也是调参依据):

复制代码
新任务进来
  → 核心线程未满?创建核心线程执行
  → 核心满了?丢进 workQueue 排队
  → 队列也满了?创建非核心线程(直到 maximumPoolSize)
  → 线程和队列都满了?执行拒绝策略

四种内置拒绝策略:

策略 行为
AbortPolicy(默认) 直接抛 RejectedExecutionException
CallerRunsPolicy 由提交任务的线程自己执行,天然形成"反压"
DiscardPolicy 静默丢弃新任务
DiscardOldestPolicy 丢弃队列头部最老的任务,重新提交当前任务

生产建议:宁可让调用方感知到异常(Abort)或降速(CallerRuns),也不要无声丢弃。


三、路线一:JDK 原生方式创建线程池

3.1 推荐写法:手动 new ThreadPoolExecutor

java 复制代码
import java.util.concurrent.*;

public class ThreadPoolDemo {

    private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
            8,                                      // 核心线程数
            16,                                     // 最大线程数
            60, TimeUnit.SECONDS,                   // 非核心线程空闲存活 60 秒
            new ArrayBlockingQueue<>(1000),         // 有界队列,容量 1000
            new NamedThreadFactory("biz-worker"),   // 自定义线程工厂
            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用方执行
    );

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            final int taskId = i;
            EXECUTOR.execute(() ->
                System.out.println(Thread.currentThread().getName() + " 执行任务 " + taskId)
            );
        }
        EXECUTOR.shutdown(); // 记得关闭
    }
}

配套的线程工厂(给线程起名字,排查问题时你会感谢自己):

java 复制代码
public class NamedThreadFactory implements ThreadFactory {
    private final AtomicInteger seq = new AtomicInteger(1);
    private final String prefix;

    public NamedThreadFactory(String prefix) {
        this.prefix = prefix;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, prefix + "-" + seq.getAndIncrement());
        t.setDaemon(false);
        return t;
    }
}

3.2 需要返回值时用 CompletableFuture

execute() 只接收 Runnable。需要拿结果或编排多个异步任务,用 CompletableFuture.supplyAsync

java 复制代码
CompletableFuture<String> future = CompletableFuture.supplyAsync(
        () -> {
            // 模拟耗时调用
            sleep(200);
            return "查询结果";
        },
        EXECUTOR  // 务必显式传入线程池,别用默认的 ForkJoinPool.commonPool()
);

// 链式编排
future.thenApply(result -> result + " -> 加工后")
      .thenAccept(System.out::println)
      .exceptionally(ex -> {
          log.error("异步任务失败", ex);
          return null;
      });

⚠️ 不传线程池时,supplyAsync 默认跑在 ForkJoinPool.commonPool() 上------它是全 JVM 共享的,一旦被慢任务占满,会拖累所有使用者。

3.3 为什么不要用 Executors 工厂方法

Executors 的便捷方法几乎都埋着雷,阿里巴巴《Java 开发手册》明确要求禁用:

方法 隐患
newFixedThreadPool / newSingleThreadExecutor 队列是无界的 LinkedBlockingQueue,任务堆积可 OOM
newCachedThreadPool / newScheduledThreadPool 最大线程数 Integer.MAX_VALUE,可能创建海量线程导致 OOM

手动 new ThreadPoolExecutor,把队列容量和最大线程数都写成有界的,才是正确姿势。


四、路线二:Spring Boot 3 中配置 ThreadPoolTaskExecutor

在 Spring 项目里更推荐用 ThreadPoolTaskExecutor------它是 ThreadPoolExecutor 的封装,额外提供:

  • 与 Spring 容器生命周期集成(优雅关闭);
  • 支持 TaskDecorator(透传 MDC、TraceId 等上下文);
  • 可直接被 @Asyncspring.task.execution 体系使用。

4.1 声明线程池 Bean

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync  // 开启 @Async 支持
public class ThreadPoolConfig {

    @Bean("bizExecutor")
    public Executor bizExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(8);
        executor.setMaxPoolSize(16);
        executor.setQueueCapacity(1000);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("biz-");          // 线程名前缀
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

        // 优雅关闭:应用停机时等待任务执行完
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(30);

        executor.initialize();
        return executor;
    }
}

4.2 配合 @Async 使用

java 复制代码
@Service
public class OrderService {

    /** 指定使用名为 bizExecutor 的线程池;不指定则用默认池 */
    @Async("bizExecutor")
    public CompletableFuture<Void> sendOrderNotice(Long orderId) {
        // 发短信、发邮件、推送等耗时旁路逻辑
        return CompletableFuture.completedFuture(null);
    }

    /** 也可以只声明 void,但拿不到完成信号 */
    @Async("bizExecutor")
    public void auditLog(String action) {
        // ...
    }
}

@Async 的四个经典失效场景(排查清单):

  1. 同类内部调用this.sendOrderNotice(...) 绕过了代理,异步不生效。解法:注入自身代理、拆到另一个 Bean,或用 AopContext.currentProxy()
  2. 方法不是 public:Spring AOP 代理的默认限制;
  3. 调用方没有处理返回值异常void 异步方法的异常默认被吞掉,需要配置 AsyncUncaughtExceptionHandler
  4. 忘了 @EnableAsync

异常处理器示例:

java 复制代码
@Configuration
public class AsyncExceptionHandler implements AsyncConfigurer {

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) ->
            log.error("异步方法 {} 执行异常, 参数: {}", method.getName(), Arrays.toString(params), ex);
    }
}

4.3 线程上下文透传(TaskDecorator)

异步线程拿不到主线程的 MDC / TraceId,日志链路会断。用 TaskDecorator 包一层:

java 复制代码
executor.setTaskDecorator(runnable -> {
    // 提交时:在调用方线程抓取上下文
    Map<String, String> context = MDC.getCopyOfContextMap();
    return () -> {
        try {
            // 执行时:在线程池线程恢复上下文
            if (context != null) {
                MDC.setContextMap(context);
            }
            runnable.run();
        } finally {
            MDC.clear();  // 线程会复用,必须清理
        }
    };
});

五、路线三:用 Spring Boot 3 配置属性快速配置

不想写配置类,可以直接在 application.yml 里配置 Spring Boot 的默认异步线程池TaskExecutionAutoConfiguration 自动装配):

yaml 复制代码
spring:
  task:
    execution:
      pool:
        core-size: 8            # 核心线程数
        max-size: 16            # 最大线程数
        queue-capacity: 1000    # 队列容量
        keep-alive: 60s         # 非核心线程空闲存活时间
        allow-core-thread-timeout: false
      thread-name-prefix: async-
      shutdown:
        await-termination: true   # 优雅关闭
        await-termination-period: 30s

配置完成后,不指定池名的 @Async 方法、TaskExecutor 类型的注入点都会使用这个池。

定时任务线程池是另一套独立配置:spring.task.scheduling.pool.size 等,和异步池互不影响。


六、多业务线程池隔离

不同业务共用一个池,慢业务会把快业务饿死。推荐按业务域拆池:

java 复制代码
@Configuration
public class ThreadPoolConfig {

    /** 订单业务池 */
    @Bean("orderExecutor")
    public Executor orderExecutor() {
        return buildExecutor("order-", 8, 16, 1000);
    }

    /** 报表导出池:重 IO、可排队 */
    @Bean("reportExecutor")
    public Executor reportExecutor() {
        return buildExecutor("report-", 4, 8, 5000);
    }

    private Executor buildExecutor(String prefix, int core, int max, int queue) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(core);
        executor.setMaxPoolSize(max);
        executor.setQueueCapacity(queue);
        executor.setThreadNamePrefix(prefix);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(30);
        executor.initialize();
        return executor;
    }
}

使用时按名注入:

java 复制代码
@Async("orderExecutor")
public void handleOrder(Long orderId) { ... }

// 或直接注入
@Resource(name = "reportExecutor")
private Executor reportExecutor;

七、参数怎么定:一个实用的起点

没有银弹公式,但可以用经典经验作为初始值,再压测调整:

  • CPU 密集型 (计算、编解码):核心数 ≈ CPU 核数 + 1
  • IO 密集型 (HTTP/RPC/DB):核心数 ≈ CPU 核数 × 2(或按 核数 × (1 + 平均等待时间/平均计算时间) 估算);
  • 混合型:拆成两个池分别配置。

比公式更重要的是两件事:

  1. 队列必须有界,容量按"可接受的最大堆积延迟 × 消费速率"倒推;
  2. 上线后看监控调参,而不是拍脑袋。

八、线程池监控

线程池出问题往往有前兆:队列越积越长、活跃线程打满。把指标暴露出来:

8.1 简单定时打印

java 复制代码
@Component
@RequiredArgsConstructor
public class ThreadPoolMonitor {

    @Resource(name = "bizExecutor")
    private Executor bizExecutor;

    @Scheduled(fixedRate = 10_000)
    public void report() {
        if (bizExecutor instanceof ThreadPoolTaskExecutor taskExecutor) {
            ThreadPoolExecutor pool = taskExecutor.getThreadPoolExecutor();
            log.info("线程池状态 | 活跃: {} | 池大小: {} | 队列: {} | 已完成: {}",
                    pool.getActiveCount(),
                    pool.getPoolSize(),
                    pool.getQueue().size(),
                    pool.getCompletedTaskCount());
        }
    }
}

8.2 接入 Micrometer(Spring Boot Actuator)

java 复制代码
import io.micrometer.core.instrument.binder.ExecutorServiceMetrics;

@Bean("bizExecutor")
public Executor bizExecutor(MeterRegistry registry) {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    // ... 参数配置省略
    executor.initialize();

    // 把指标绑定到 MeterRegistry,即可通过 /actuator/metrics 查询
    ExecutorServiceMetrics.monitor(registry,
            executor.getThreadPoolExecutor(), "bizExecutor");
    return executor;
}

之后可查询 executor_active_threadsexecutor_queued_tasks 等指标,配合告警规则使用。


九、优雅关闭

Spring Boot 3 应用收到停机信号(如 K8s 滚动更新)时,如果线程池里还有任务在跑,直接杀进程就会丢任务。两个关键点:

  1. 线程池侧:setWaitForTasksToCompleteOnShutdown(true) + setAwaitTerminationSeconds(30)(或 yaml 中 spring.task.execution.shutdown.*);
  2. Web 容器侧:开启 server.shutdown=graceful,先停止接收新请求:
yaml 复制代码
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 40s   # 略大于线程池等待时间

十、常见坑汇总

说明与对策
Executors.newXxx 无界队列/无上限线程,OOM 风险;手动构建有界池
CompletableFuture 不传线程池 默认共用 ForkJoinPool.commonPool();务必显式传池
@Async 同类调用失效 走代理才生效;拆 Bean 或注入自身代理
异步异常被吞 void 方法配 AsyncUncaughtExceptionHandler,或改用 CompletableFuture 返回
线程无名 出问题时线程 dump 全是 pool-1-thread-3;统一设置 threadNamePrefix
MDC/TraceId 丢失 TaskDecorator 透传并在 finally 里清理
所有业务共用一个池 按业务域隔离,慢任务不拖累快任务
忘了关闭线程池 非 Spring 管理的池要 shutdown();Spring 管理的交给容器生命周期

十一、写在最后

关于虚拟线程(Virtual Threads)补充一句:它是 JDK 21 正式特性 (JDK 19/20 为预览),JDK 17 还用不上。不过 Spring Boot 3.2+ 已提前提供了开关 spring.threads.virtual.enabled=true,升级到 JDK 21 后可一键让异步任务、Tomcat 请求线程切换到虚拟线程。对于 IO 密集型系统,这是一次值得关注的范式变化------但在 JDK 17 上,把传统线程池配好、监控好,依然是最稳的基本功。

一句话总结:手动构建有界池 + 自定义线程名 + 按业务隔离 + 优雅关闭 + 暴露监控指标,做到这五点,线程池就不会成为系统里的黑盒。


参考:Spring Boot 3.x 官方文档 Task Execution 章节、Doug Lea《Java 并发编程实战》、阿里巴巴《Java 开发手册》。

相关推荐
十五喵源码网2 天前
基于springboot2+vue2的疾病防控综合系统
java·毕业设计·springboot·论文笔记
玉&心3 天前
GraalVM 21 Native Image 编译踩坑全记录:从 class 初始化冲突到成功构建
ai·springboot·graalvm·rag
十五喵源码网3 天前
基于SpringBoot2+vue2的健身房管理系统
java·毕业设计·springboot·论文笔记
記億揺晃着的那天4 天前
从 0 到 2,000+ 次提交:ERP 四年的架构演进
架构·springboot·erp
QQ_21696290964 天前
【源码编号:project93375】SpringBoot汽车维修管理信息系统:客户车辆、维修预约、工单派发、配件结算全流程实战
java·spring boot·后端·汽车·springboot·需求分析
罗超驿6 天前
SpringMVC请求参数获取与响应处理|全套注解详解
springmvc·springboot·javaweb·请求参数·后端开发
孔明click336 天前
不想写代码,但想要集成一个登录页面?Sa-Token-Quick-Login 帮你实现!
java·sa-token·开源·springboot·登录·权限·权限认证
Java牛马7 天前
SpringBoot Starter 依赖相关总结
java·spring·springboot·starter·自动装配·依赖