Java 线程池使用指南:基于 Spring Boot 3.x + JDK 17 的入门与实践

定位:给 Java 新手看的线程池入门博客。读完以后,你应该能理解线程池为什么存在、核心参数怎么配置、Spring Boot 3.x 项目里怎么用,以及常见坑怎么避开。

参考资料

本文参考了官方文档和主流技术资料:

文中的代码示例均为通用示例,用于说明线程池设计思路,不依赖任何特定业务系统。

本文中的 Spring 3.x 指 Spring Boot 3.x。Spring Boot 3.x 基于 Spring Framework 6.x,并要求 Java 17 或更高版本。本文选择 JDK 17,是因为它仍是很多企业项目的长期支持版本;虚拟线程属于 JDK 21 之后更常见的选择,本文不把它作为主线,以免把"线程池入门"讲得过早复杂化。

一、为什么要用线程池?

如果每来一个任务就 new Thread(),看起来很直观:

java 复制代码
new Thread(() -> doSomething()).start();

但在真实后端服务里,这种写法有几个明显问题:

  1. 线程创建和销毁有成本,任务多时会浪费 CPU 和内存。
  2. 线程数量不受控制,流量高峰时可能把机器打满。
  3. 缺少统一的队列、拒绝策略、异常处理和监控指标。
  4. 线程名不清晰,线上排查问题时很难定位是哪类任务。

线程池的价值可以一句话概括:把"无限制创建线程"变成"有限资源内有序处理任务"。

它解决的是两个问题:

  • 复用线程,降低频繁创建线程的成本。
  • 限制并发量和排队量,保护应用和下游资源。

二、先认识线程池的核心接口

Java 线程池常见接口和类如下:

名称 作用
Executor 最基础的任务执行接口,只有 execute(Runnable)
ExecutorService Executor 基础上增加生命周期管理、submitFuture 等能力
ThreadPoolExecutor 最常用的线程池实现类,可配置核心线程数、最大线程数、队列、拒绝策略
ScheduledExecutorService 支持延迟任务、周期任务
ForkJoinPool 面向分治任务和并行计算,也常被 CompletableFuture 默认异步方法使用
ThreadPoolTaskExecutor Spring 对 ThreadPoolExecutor 的封装,适合 Spring Boot 项目通过 Bean 管理

新手可以先记住两句话:

  • 普通 Java 项目里,核心是 ThreadPoolExecutor
  • Spring Boot 项目里,优先把 ThreadPoolTaskExecutor 配成 Bean,再通过 @AsyncCompletableFuture 使用。

三、ThreadPoolExecutor 的 7 个核心参数

ThreadPoolExecutor 最完整的构造方法如下:

java 复制代码
ExecutorService executor = new ThreadPoolExecutor(
        10,
        20,
        60L,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(1000),
        runnable -> {
            Thread thread = new Thread(runnable);
            thread.setName("order-worker-" + thread.getId());
            return thread;
        },
        new ThreadPoolExecutor.CallerRunsPolicy()
);

每个参数的含义:

参数 含义 新手理解
corePoolSize 核心线程数 日常保留多少个工人
maximumPoolSize 最大线程数 高峰期最多允许多少个工人
keepAliveTime 非核心线程空闲存活时间 临时工空闲多久后离开
TimeUnit 时间单位 秒、毫秒、分钟等
workQueue 任务队列 工人忙不过来时,任务先排队
ThreadFactory 线程工厂 给线程命名、设置是否守护线程等
RejectedExecutionHandler 拒绝策略 队列满且线程也满时怎么处理

其中最容易配错的是前 5 个:核心线程、最大线程、队列、存活时间、拒绝策略。

四、线程池到底怎么执行任务?

这是线程池最关键的逻辑。根据 JDK 官方文档和 Spring 文档,任务提交后的流程可以这样理解:

  1. 如果当前运行线程数小于 corePoolSize,直接创建新线程执行任务。
  2. 如果核心线程已满,任务进入队列 workQueue
  3. 如果队列也满了,并且当前线程数小于 maximumPoolSize,创建非核心线程执行任务。
  4. 如果线程数已经达到 maximumPoolSize,队列也满了,触发拒绝策略。

用更生活化的话说:

text 复制代码
先找核心工人干活
核心工人都忙了,就让任务排队
队列也排满了,就临时加工人
工人也加到上限了,就按拒绝策略处理

这里有一个非常重要的点:不是一上来就把线程扩到 maximumPoolSize

很多新手以为配置了:

java 复制代码
corePoolSize = 10
maximumPoolSize = 100
queueCapacity = 10000

线程池就会很快扩到 100 个线程。实际上不会。因为核心线程满了以后,任务会优先进入队列。只有队列满了,才会继续创建核心线程之外的线程。

所以队列容量越大,线程池越不容易扩容到最大线程数。

五、为什么不建议直接用 Executors?

JDK 提供了 Executors.newFixedThreadPoolnewCachedThreadPoolnewSingleThreadExecutor 等工厂方法。它们适合教学和简单场景,但在生产服务里要谨慎使用。

Alibaba Java Coding Guidelines 明确建议:线程池应通过 ThreadPoolExecutor 创建,而不是直接使用 Executors,这样开发者能明确线程池运行规则,规避资源耗尽风险。

常见风险如下:

方法 隐藏风险
Executors.newFixedThreadPool(n) 使用无界队列,任务堆积过多可能 OOM
Executors.newSingleThreadExecutor() 同样可能使用很大的队列,单线程处理不过来时任务持续堆积
Executors.newCachedThreadPool() 最大线程数近似不受控,高峰期可能创建大量线程
Executors.newScheduledThreadPool(n) 延迟/周期任务如果执行过慢,也可能堆积

生产环境更推荐显式配置:

java 复制代码
ExecutorService executor = new ThreadPoolExecutor(
        8,
        16,
        60L,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(500),
        runnable -> {
            Thread thread = new Thread(runnable);
            thread.setName("biz-worker-" + thread.getId());
            return thread;
        },
        new ThreadPoolExecutor.CallerRunsPolicy()
);

显式配置的好处是:线程数、队列长度、拒绝策略都摆在明面上,线上容量风险更可控。

六、拒绝策略怎么选?

当线程池已经达到最大线程数,并且队列也满了,新任务就会被拒绝。JDK 默认提供 4 种策略:

策略 行为 适合场景
AbortPolicy 直接抛 RejectedExecutionException 任务不能丢,提交方必须感知失败
CallerRunsPolicy 由提交任务的线程自己执行 希望反压上游,降低提交速度
DiscardPolicy 静默丢弃新任务 极少使用,除非任务确实可丢
DiscardOldestPolicy 丢弃队列里最老的任务,再尝试提交 实时性强、旧任务价值低的场景

例如,一个通知分发线程池可以使用 AbortPolicy,让调用方明确感知"任务提交失败"。

java 复制代码
@Bean(name = "notificationDispatchExecutor")
public ThreadPoolTaskExecutor notificationDispatchExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(20);
    executor.setMaxPoolSize(40);
    executor.setQueueCapacity(500);
    executor.setThreadNamePrefix("notification-dispatch-");
    executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.AbortPolicy());
    executor.initialize();
    return executor;
}

这类任务如果发送失败,需要让调用方知道失败并做重试或释放状态,因此直接抛异常比静默丢任务更安全。

七、Spring Boot 项目中推荐怎么用?

在 Spring Boot 项目中,最常见做法是:

  1. 在配置类中开启 @EnableAsync
  2. 定义多个 ThreadPoolTaskExecutor Bean。
  3. 业务方法通过 @Async("线程池Bean名") 指定线程池。
  4. 需要手动编排异步任务时,把线程池传给 CompletableFuture.runAsyncsupplyAsync

1. 定义线程池 Bean

下面是一个适用于 Spring Boot 3.x 的典型配置:

java 复制代码
@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "statusRefreshExecutor")
    public ThreadPoolTaskExecutor statusRefreshExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(20);
        executor.setMaxPoolSize(40);
        executor.setQueueCapacity(3000);
        executor.setThreadNamePrefix("status-refresh-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(30);
        executor.initialize();
        return executor;
    }
}

这段配置表达了几个信息:

  • 这是状态刷新任务专用线程池。
  • 常态并发 20,峰值最高 40。
  • 最多允许 3000 个任务排队。
  • 线程名前缀清晰,日志和线程 dump 里容易定位。
  • 服务关闭时最多等待 30 秒,让已经提交的任务尽量完成。

如果你使用的是 Spring Boot 3.2 或更高版本,也可以通过 ThreadPoolTaskExecutorBuilder 创建线程池;但新手先掌握上面这种 Bean 风格配置更直观,也更容易理解每个参数对应的底层含义。

2. 用 @Async 执行异步方法

业务代码中可以这样使用:

java 复制代码
@Async("notificationDispatchExecutor")
public void sendNotification(...) {
    // 异步发送通知逻辑
}

Spring 官方文档说明,@Async 方法被调用后,调用方会立即返回,真实执行会被提交给 Spring TaskExecutor。如果使用 @Async("notificationDispatchExecutor"),Spring 会按这个名字找到对应的 Executor Bean。

但是 @Async 有几个新手很容易踩的坑:

  1. 必须先启用 @EnableAsync
  2. @Async 标注的方法必须通过 Spring 代理调用,同一个类内部 this.xxx() 调用不会生效。
  3. void 返回值的方法内部异常不会直接抛给调用方,建议记录日志或配置 AsyncUncaughtExceptionHandler
  4. 如果需要拿返回值,返回类型可以用 FutureCompletableFuture
  5. 不建议把事务边界和异步边界混在一起,需要明确异步线程里是否还能拿到上下文。

3. 用 CompletableFuture 指定线程池

需要手动编排异步任务时,可以把指定线程池传给 CompletableFuture

java 复制代码
private final ThreadPoolTaskExecutor statusRefreshExecutor;

public StatusRefreshService(
        @Qualifier("statusRefreshExecutor") ThreadPoolTaskExecutor statusRefreshExecutor) {
    this.statusRefreshExecutor = statusRefreshExecutor;
}

private void submitRefreshTask(List<Long> recordIds, String refreshType) {
    try {
        CompletableFuture.runAsync(
                () -> refreshStatus(recordIds, refreshType),
                statusRefreshExecutor
        ).exceptionally(ex -> {
            log.error("异步刷新状态失败, refreshType={}, size={}",
                    refreshType, recordIds.size(), ex);
            return null;
        });
    } catch (RejectedExecutionException ex) {
        log.error("线程池已满,异步刷新状态任务提交失败, refreshType={}, size={}",
                refreshType, recordIds.size(), ex);
    }
}

这个例子有三个优点:

  • 没有使用默认线程池,而是明确指定 statusRefreshExecutor
  • 使用 exceptionally 记录异步执行过程中的异常。
  • 捕获了任务提交阶段可能出现的 RejectedExecutionException

这是比较适合生产环境的写法。

八、CompletableFuture.runAsync 不传线程池有什么问题?

很多代码里会出现类似写法:

java 复制代码
CompletableFuture.runAsync(() -> reportService.generateReport(reportId));

这段代码能运行,但从工程治理角度看,不如显式传入业务线程池。

原因是:CompletableFuture 的异步方法如果不指定 Executor,通常会使用默认异步执行设施,也就是公共 ForkJoinPool.commonPool()。这会带来几个问题:

  1. 多个业务共用公共线程池,互相影响。
  2. 线程名不带业务语义,排查问题困难。
  3. 队列、拒绝策略、监控都不容易按业务隔离。
  4. 如果任务里有阻塞 IO,可能影响公共池中其他计算型任务。

更推荐的写法是:

java 复制代码
private final ThreadPoolTaskExecutor reportGenerateExecutor;

public ReportTaskService(
        @Qualifier("reportGenerateExecutor") ThreadPoolTaskExecutor reportGenerateExecutor) {
    this.reportGenerateExecutor = reportGenerateExecutor;
}

public boolean submitReportTask(Long reportId) {
    // 前置校验、状态更新等同步逻辑
    CompletableFuture.runAsync(
            () -> reportService.generateReport(reportId),
            reportGenerateExecutor
    ).exceptionally(ex -> {
        log.error("异步生成报表失败, reportId={}", reportId, ex);
        return null;
    });
    return true;
}

这样线程池资源属于"报表生成"业务,不会和其他异步任务混在一起。

九、线程池大小应该怎么估算?

线程池没有万能参数。需要先判断任务类型。

1. CPU 密集型任务

特点:主要消耗 CPU,比如加密、压缩、复杂计算、图片处理。

建议:

text 复制代码
线程数 ≈ CPU 核数 或 CPU 核数 + 1

原因:线程太多只会增加上下文切换,不会让 CPU 变多。

2. IO 密集型任务

特点:大量等待数据库、Redis、HTTP、文件、第三方接口。

建议:

text 复制代码
线程数可以大于 CPU 核数

但不能只看 CPU,还要看:

  • 数据库连接池大小。
  • 下游接口限流。
  • 单任务平均耗时。
  • 峰值任务量。
  • 是否允许任务排队。

一个常见估算公式是:

text 复制代码
线程数 ≈ CPU 核数 × (1 + 等待时间 / 计算时间)

例如,一个任务 90ms 在等数据库或 HTTP,10ms 在做本地计算,那么:

text 复制代码
线程数 ≈ CPU 核数 × (1 + 90 / 10) = CPU 核数 × 10

这个公式只适合估算初始值,最终还是要压测和线上监控来调整。

3. 不要让线程池超过下游承载能力

假设数据库连接池最大只有 30 个连接,但你把某个数据库写入线程池配置成 200 个线程,结果很可能不是更快,而是更多线程阻塞在拿连接上,甚至拖垮数据库。

线程池大小要和下游资源一起看:

text 复制代码
线程池并发数 <= 下游可承受并发数

这是后端性能优化里很朴素但很重要的一条。

十、任务队列怎么选?

常见队列:

队列 特点 适合场景
ArrayBlockingQueue 有界数组队列 固定容量、内存可控
LinkedBlockingQueue 链表队列,可有界也可无界 常用,但一定要指定容量
SynchronousQueue 不存储任务,直接移交线程 类似 newCachedThreadPool,需要谨慎控制最大线程数
PriorityBlockingQueue 优先级队列 需要按优先级执行任务
DelayQueue 延迟队列 延迟任务、超时任务

生产项目里最常见的是:

java 复制代码
new LinkedBlockingQueue<>(1000)

关键是括号里的容量。不要轻易使用无界队列。

十一、线程池一定要关闭

JDK 官方文档指出,ExecutorService 不再使用时应该关闭,以便释放资源。

普通 Java 代码里建议这样写:

java 复制代码
public void executeBatch(List<Runnable> tasks) {
    ExecutorService executor = new ThreadPoolExecutor(
            4,
            8,
            60L,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(200),
            runnable -> {
                Thread thread = new Thread(runnable);
                thread.setName("batch-worker-" + thread.getId());
                return thread;
            },
            new ThreadPoolExecutor.CallerRunsPolicy()
    );

    try {
        for (Runnable task : tasks) {
            executor.execute(task);
        }
    } finally {
        shutdownAndAwaitTermination(executor);
    }
}

private void shutdownAndAwaitTermination(ExecutorService executor) {
    executor.shutdown();
    try {
        if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
            executor.shutdownNow();
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                log.error("线程池未能正常关闭");
            }
        }
    } catch (InterruptedException ex) {
        executor.shutdownNow();
        Thread.currentThread().interrupt();
    }
}

Spring 管理的 ThreadPoolTaskExecutor 通常由 Spring 容器负责生命周期,但如果你自己 new ThreadPoolExecutor(),就要自己负责关闭。

如果某个组件自己创建了线程池,可以实现 Spring 的 DisposableBean,并在 destroy() 中关闭线程池:

java 复制代码
@Override
public void destroy() {
    eventDispatchExecutor.shutdown();
    notificationExecutor.shutdown();
    reportGenerateExecutor.shutdown();
}

如果要进一步增强,可以加上 awaitTermination 和中断处理,确保关闭过程更可控。

十二、线程池异常怎么处理?

线程池里的异常分为两类:

1. 任务提交失败

线程池满了,触发拒绝策略,可能抛出 RejectedExecutionException

建议:

java 复制代码
try {
    executor.execute(task);
} catch (RejectedExecutionException ex) {
    log.error("任务提交失败,线程池已满", ex);
    // 根据业务决定:返回失败、稍后重试、写入补偿表、降级处理
}

2. 任务执行失败

任务在线程池里执行时抛异常。

如果用 CompletableFuture

java 复制代码
CompletableFuture
        .runAsync(() -> doSomething(), executor)
        .exceptionally(ex -> {
            log.error("异步任务执行失败", ex);
            return null;
        });

如果用 submit

java 复制代码
Future<Result> future = executor.submit(() -> queryRemoteData());

try {
    Result result = future.get();
} catch (InterruptedException ex) {
    Thread.currentThread().interrupt();
    throw new IllegalStateException("任务被中断", ex);
} catch (ExecutionException ex) {
    log.error("异步任务执行失败", ex.getCause());
    throw new IllegalStateException("异步任务执行失败", ex.getCause());
}

注意:executesubmit 的异常表现不同。execute 中未捕获异常通常会交给线程的异常处理机制;submit 会把异常包装到 Future,需要调用 get() 时才能感知。

十三、ThreadLocal 和上下文传递要小心

很多后端项目会把用户信息、请求 ID、traceId、登录态放在 ThreadLocal 或 MDC 里。异步线程不是原调用线程,默认拿不到这些上下文。

常见问题:

  • 日志 traceId 丢失。
  • 用户信息丢失。
  • 数据权限判断异常。

如果项目使用阿里 TransmittableThreadLocal,可以通过 TtlExecutors.getTtlExecutorService(...) 包装线程池,在异步线程中传递必要上下文。

示例:

java 复制代码
ExecutorService executor = TtlExecutors.getTtlExecutorService(
        new ThreadPoolExecutor(
                8,
                16,
                60L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1000),
                runnable -> {
                    Thread thread = new Thread(runnable);
                    thread.setName("event-worker-" + thread.getId());
                    return thread;
                },
                new ThreadPoolExecutor.CallerRunsPolicy()
        )
);

但要注意:上下文传递不是越多越好。敏感信息、过期上下文、事务对象等不应该随便跨线程传播。

十四、Spring Boot 3.x + JDK 17 的完整案例:异步导出报表

假设有一个报表导出接口。用户点击"导出"后,接口不要一直阻塞到文件生成完成,而是快速返回一个 taskId;后台线程池负责生成文件,并把任务状态更新为成功或失败。

这个案例能体现线程池在真实系统里的价值:

  • 慢任务不阻塞 HTTP 请求线程。
  • 报表导出有独立线程池,不影响通知发送、状态刷新等其他异步任务。
  • 线程池满了时能让调用方感知系统繁忙。
  • 后台任务失败时有日志和任务状态兜底。

1. 请求和响应对象

JDK 17 可以使用 record 定义简单 DTO。Spring Boot 3.x 使用 Jakarta EE 规范,参数校验包名是 jakarta.validation

java 复制代码
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

import java.time.LocalDate;

public record ReportExportRequest(
        @NotBlank String reportType,
        @NotNull LocalDate startDate,
        @NotNull LocalDate endDate
) {
}

public record ReportExportResponse(Long taskId) {
}

2. 配置报表导出线程池

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.ThreadPoolExecutor;

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "reportExportExecutor")
    public ThreadPoolTaskExecutor reportExportExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(8);
        executor.setMaxPoolSize(16);
        executor.setQueueCapacity(500);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("report-export-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(30);
        executor.initialize();
        return executor;
    }
}

这里选择 AbortPolicy,是因为报表导出任务通常不能静默丢弃。如果线程池已经满了,接口层应该明确返回"系统繁忙,请稍后再试",而不是让用户以为任务已经提交成功。

3. Controller 快速返回 taskId

java 复制代码
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/reports")
public class ReportExportController {

    private final ReportExportService reportExportService;

    public ReportExportController(ReportExportService reportExportService) {
        this.reportExportService = reportExportService;
    }

    @PostMapping("/exports")
    public ReportExportResponse export(@Valid @RequestBody ReportExportRequest request) {
        Long taskId = reportExportService.createExportTask(request);
        return new ReportExportResponse(taskId);
    }
}

Controller 只负责接收请求和返回响应,不直接处理耗时的报表生成逻辑。

4. Service 创建任务并提交异步执行

java 复制代码
import org.springframework.core.task.TaskRejectedException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

@Service
public class ReportExportService {

    private final ReportTaskRepository reportTaskRepository;
    private final ReportExportAsyncService reportExportAsyncService;

    public ReportExportService(ReportTaskRepository reportTaskRepository,
                               ReportExportAsyncService reportExportAsyncService) {
        this.reportTaskRepository = reportTaskRepository;
        this.reportExportAsyncService = reportExportAsyncService;
    }

    @Transactional(rollbackFor = Exception.class)
    public Long createExportTask(ReportExportRequest request) {
        validateDateRange(request);

        Long taskId = reportTaskRepository.createPendingTask(request);
        try {
            reportExportAsyncService.generateReport(taskId, request);
        } catch (TaskRejectedException ex) {
            reportTaskRepository.markFailed(taskId, "系统繁忙,任务提交失败");
            throw new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS, "导出任务繁忙,请稍后再试", ex);
        }
        return taskId;
    }

    private void validateDateRange(ReportExportRequest request) {
        if (request.endDate().isBefore(request.startDate())) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "结束日期不能早于开始日期");
        }
    }
}

这段代码里,ReportTaskRepository 可以理解为任务表的数据访问对象。真实项目里,你可以用 JPA、MyBatis、JDBC 或任何已有持久化方案实现它。

5. 异步服务执行耗时任务

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;

@Slf4j
@Service
public class ReportExportAsyncService {

    private final ReportGenerator reportGenerator;
    private final ReportTaskRepository reportTaskRepository;

    public ReportExportAsyncService(ReportGenerator reportGenerator,
                                    ReportTaskRepository reportTaskRepository) {
        this.reportGenerator = reportGenerator;
        this.reportTaskRepository = reportTaskRepository;
    }

    @Async("reportExportExecutor")
    public CompletableFuture<Void> generateReport(Long taskId, ReportExportRequest request) {
        try {
            reportTaskRepository.markRunning(taskId);
            Path filePath = reportGenerator.generate(request);
            reportTaskRepository.markSuccess(taskId, filePath.toString());
        } catch (Exception ex) {
            log.error("报表导出失败, taskId={}, reportType={}", taskId, request.reportType(), ex);
            reportTaskRepository.markFailed(taskId, "报表生成失败");
        }
        return CompletableFuture.completedFuture(null);
    }
}

这里把异步方法放到单独的 ReportExportAsyncService,可以避免同类内部调用导致 @Async 不生效。任务执行失败时不要只写日志,还要把任务状态标记为失败,否则用户只能看到"处理中"一直不结束。

十五、业务线程池隔离怎么理解?

真实系统里通常会按业务场景拆分线程池,例如:

java 复制代码
@Bean(name = "reportGenerateExecutor")
public ThreadPoolTaskExecutor reportGenerateExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(20);
    executor.setMaxPoolSize(40);
    executor.setQueueCapacity(1000);
    executor.setThreadNamePrefix("report-generate-");
    executor.initialize();
    return executor;
}

@Bean(name = "retrySyncExecutor")
public ThreadPoolTaskExecutor retrySyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(1);
    executor.setMaxPoolSize(1);
    executor.setQueueCapacity(2000);
    executor.setThreadNamePrefix("retry-sync-");
    executor.initialize();
    return executor;
}

这体现了一个重要原则:不同业务场景用不同线程池隔离。

为什么要隔离?

  • 报表生成任务变慢,不应该拖慢通知发送。
  • 文件同步任务堆积,不应该影响状态刷新。
  • 批量刷新任务高峰,不应该占满所有异步线程。

retrySyncExecutor 配成单线程也很有业务含义:重试同步任务可能要求顺序执行,或者不希望并发太高影响下游。

十六、常见错误清单

1. 队列无界

错误写法:

java 复制代码
new LinkedBlockingQueue<>()

建议:

java 复制代码
new LinkedBlockingQueue<>(1000)

2. 线程池没有业务命名

错误表现:

text 复制代码
pool-1-thread-1
pool-2-thread-3

建议:

java 复制代码
executor.setThreadNamePrefix("data-monitor-status-pool-");

线程名应该能看出业务含义。

3. CompletableFuture 不指定线程池

不推荐:

java 复制代码
CompletableFuture.runAsync(() -> doSomething());

推荐:

java 复制代码
CompletableFuture.runAsync(() -> doSomething(), bizExecutor);

4. @Async 同类内部调用

不生效:

java 复制代码
public void methodA() {
    this.methodB();
}

@Async("bizExecutor")
public void methodB() {
    // 不会异步执行
}

建议把异步方法放到另一个 Spring Bean,通过代理调用。

5. 异步任务异常无人感知

不推荐:

java 复制代码
CompletableFuture.runAsync(() -> riskyOperation(), executor);

推荐:

java 复制代码
CompletableFuture
        .runAsync(() -> riskyOperation(), executor)
        .exceptionally(ex -> {
            log.error("异步任务执行失败", ex);
            return null;
        });

6. 在循环里无节制提交任务

不推荐:

java 复制代码
for (Item item : items) {
    executor.execute(() -> process(item));
}

如果 items 很大,瞬间提交大量任务会冲击线程池和队列。

建议:

  • 分批提交。
  • 控制分页大小。
  • 使用有界队列和拒绝策略。
  • 必要时加限流或信号量。

7. 线程池嵌套调用导致死等

比如线程池只有 2 个线程,任务 A 和 B 都在等同一个线程池里的子任务完成,但子任务没有线程可用,就可能卡住。

建议:

  • 避免在同一个小线程池内同步等待子任务。
  • 计算型任务和 IO 型任务隔离。
  • 不要在异步任务里随意 join() 大量同池任务。

8. 忽略中断

不推荐:

java 复制代码
try {
    Thread.sleep(1000);
} catch (InterruptedException ex) {
    log.warn("被中断", ex);
}

推荐:

java 复制代码
try {
    Thread.sleep(1000);
} catch (InterruptedException ex) {
    Thread.currentThread().interrupt();
    log.warn("任务被中断", ex);
}

捕获 InterruptedException 后要恢复中断标记,方便上层感知。

十七、线程池上线前检查表

上线前可以按下面清单检查:

  • 是否明确了任务类型:CPU 密集型还是 IO 密集型?
  • 是否配置了有界队列?
  • 是否设置了业务化线程名前缀?
  • 是否设置了合适的拒绝策略?
  • 是否捕获并记录异步任务异常?
  • 是否处理了 RejectedExecutionException
  • 是否考虑了下游数据库、Redis、HTTP 接口承载能力?
  • 是否避免 CompletableFuture 默认公共线程池?
  • 是否避免 @Async 同类内部调用?
  • 是否需要传递 traceId、tenantId、userId 等上下文?
  • 非 Spring 管理的线程池是否会关闭?
  • 是否有监控指标:活跃线程数、队列长度、任务完成数、拒绝次数?

十八、怎么监控线程池?

生产上至少应该关注:

指标 意义
poolSize 当前线程数
activeCount 正在执行任务的线程数
queueSize 当前排队任务数
completedTaskCount 已完成任务数
taskCount 总任务数
reject count 拒绝次数,需要自定义统计

ThreadPoolTaskExecutor 提供了 getPoolSize()getActiveCount()getQueueSize() 等方法,可以暴露给监控系统。

简单示例:

java 复制代码
public void logExecutorMetrics(ThreadPoolTaskExecutor executor, String executorName) {
    log.info("executorName={}, poolSize={}, activeCount={}, queueSize={}",
            executorName,
            executor.getPoolSize(),
            executor.getActiveCount(),
            executor.getQueueSize());
}

如果发现 queueSize 长期接近上限,说明任务生产速度大于消费速度,需要扩容、限流、拆分线程池、优化任务耗时,或者检查下游资源。

相关推荐
爱喝水的鱼丶1 小时前
SAP-ABAP:SAP多表连接视图实战:内连接/外连接配置逻辑与性能优化技巧
运维·开发语言·学习·性能优化·sap·abap
星恒随风1 小时前
C++ 类和对象入门(六):友元、内部类、匿名对象和编译器优化
开发语言·c++·笔记·学习·状态模式
Elias不吃糖1 小时前
RabbitMQ vs Kafka 简单总结
java·分布式·kafka·rabbitmq
ch.ju1 小时前
Java Programming Chapter 4——Error in compilation: it cannot be overwritten.
java·开发语言
xxie1237941 小时前
参数Parameter,形参Formal Parameter,实参Actual Argument
开发语言·python
小短腿的代码世界1 小时前
高性能订单路由与智能拆单算法:Qt在量化交易系统中的核心架构——毫秒级延迟下如何隐藏你的交易意图?
开发语言·qt·架构
nice_lcj5201 小时前
排序(4)-归并排序专题——归并排序的分治美学
java·数据结构·算法·排序算法
阿正的梦工坊1 小时前
【Rust】20-Rust 编译器架构与 MIR/LLVM 优化管线
开发语言·架构·rust
在放️1 小时前
Python 爬虫 · XML、xpath 与 lxml 模块基础
开发语言·爬虫·python