Skip to content
第 39 / 250 章后端⏱ 10 分钟阅读

第 39 章:线程池

学习目标

  • 理解为什么必须用线程池
  • 掌握七大核心参数
  • 学会参数调优与拒绝策略

一、为什么必须用线程池?

java
// ❌ 每来一个请求 new 一个线程
public void handleRequest() {
    new Thread(() -> process()).start();
}

三个致命问题

问题说明
创建销毁开销大创建线程要调操作系统,约 1ms + 1MB 栈内存
无法限流请求暴涨时线程数无上限 → OOM
无法管理拿不到执行状态,无法监控、无法优雅关闭

阿里规约:线程资源必须通过线程池提供,不允许显式创建线程。

二、七大核心参数

java
public ThreadPoolExecutor(
    int corePoolSize,                    // ① 核心线程数
    int maximumPoolSize,                 // ② 最大线程数
    long keepAliveTime,                  // ③ 空闲线程存活时间
    TimeUnit unit,                       // ④ 时间单位
    BlockingQueue<Runnable> workQueue,   // ⑤ 任务队列
    ThreadFactory threadFactory,         // ⑥ 线程工厂
    RejectedExecutionHandler handler     // ⑦ 拒绝策略
)

任务提交流程(面试必考)

关键理解队列满了才会创建非核心线程。所以如果你用了无界队列(LinkedBlockingQueue 默认容量 Integer.MAX_VALUE),maximumPoolSize 永远不会生效

三、任务队列选择

队列特点适用
ArrayBlockingQueue有界,数组推荐,能防 OOM
LinkedBlockingQueue默认无界,链表⚠️ 必须传容量参数
SynchronousQueue不存储,直接交给线程任务少但要快速响应
PriorityBlockingQueue优先级排序任务有优先级
DelayQueue延迟执行定时任务

四、拒绝策略

java
// ① AbortPolicy(默认):抛 RejectedExecutionException
new ThreadPoolExecutor.AbortPolicy();

// ② CallerRunsPolicy:让提交任务的线程自己执行 —— 天然限流,推荐
new ThreadPoolExecutor.CallerRunsPolicy();

// ③ DiscardPolicy:静默丢弃 —— 危险,任务丢了都不知道
new ThreadPoolExecutor.DiscardPolicy();

// ④ DiscardOldestPolicy:丢掉队列最老的任务,再重试
new ThreadPoolExecutor.DiscardOldestPolicy();

// ⑤ 自定义:记日志 + 落库补偿
new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        log.error("线程池已满,任务被拒绝。活跃={}, 队列={}",
                executor.getActiveCount(), executor.getQueue().size());
        // 落库、发告警、写 MQ 补偿...
    }
};

推荐 CallerRunsPolicy:调用方线程去执行任务会变慢,自然减缓了提交速度,形成背压,比直接丢任务安全。

五、为什么不用 Executors?

java
// ❌ 阿里规约明确禁止
Executors.newFixedThreadPool(10);      // 队列无界 → 任务堆积 OOM
Executors.newCachedThreadPool();       // 最大线程数 Integer.MAX_VALUE → 线程爆炸 OOM
Executors.newSingleThreadExecutor();   // 队列无界 → OOM
Executors.newScheduledThreadPool(1);   // 队列无界 → OOM

正确做法:手动 new

java
public class ThreadPoolConfig {

    public static ThreadPoolExecutor create() {
        return new ThreadPoolExecutor(
                8,                                        // ① 核心线程
                16,                                       // ② 最大线程
                60L, TimeUnit.SECONDS,                    // ③ 空闲 60 秒回收
                new ArrayBlockingQueue<>(1000),           // ④ 有界队列,防 OOM
                new ThreadFactoryBuilder()
                        .setNameFormat("order-pool-%d")   // ⑤ 命名!排查问题的关键
                        .build(),
                new ThreadPoolExecutor.CallerRunsPolicy() // ⑥ 背压
        );
    }
}

⑤ 线程命名有多重要? 线上出问题看线程 dump,pool-1-thread-5 你根本不知道是哪个业务;order-pool-5 一眼定位。

六、参数怎么定?

CPU 密集型(计算为主)

corePoolSize = CPU 核数 + 1

线程数超过核数只会增加上下文切换开销。+1 是为了在某线程偶尔阻塞时仍能跑满 CPU。

IO 密集型(数据库、网络调用为主)

corePoolSize = CPU 核数 × (1 + 平均等待时间 / 平均计算时间)

实际举例:8 核机器,接口平均耗时 100ms,其中 CPU 计算 5ms,等数据库 95ms:

8 × (1 + 95/5) = 8 × 20 = 160

⚠️ 公式只是起点。真实值必须靠压测确定:逐步加大线程数,观察 QPS 和 RT,找到拐点。

java
int cpuCores = Runtime.getRuntime().availableProcessors();

七、提交任务:execute vs submit

java
// execute:无返回值,异常直接抛到 UncaughtExceptionHandler
pool.execute(() -> doWork());

// submit:返回 Future,异常被封装在 Future 里
Future<String> future = pool.submit(() -> "结果");
String result = future.get();               // ① 阻塞直到有结果
String r2 = future.get(3, TimeUnit.SECONDS); // ② 超时控制,推荐

⚠️ submit 的隐形陷阱:任务抛异常时不会打印任何日志,异常被藏在 Future 里。如果你不调 get(),异常就永远消失了。 对策:任务内部自己 try-catch,或者一定要调 get()

八、优雅关闭

java
public void shutdownGracefully(ExecutorService pool) {
    pool.shutdown();                        // ① 不再接受新任务,已提交的继续执行
    try {
        if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {   // ② 等 60 秒
            pool.shutdownNow();                                // ③ 强制中断
            if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
                log.error("线程池未能正常关闭");
            }
        }
    } catch (InterruptedException e) {
        pool.shutdownNow();
        Thread.currentThread().interrupt();                    // ④ 恢复中断
    }
}
方法行为
shutdown()温和:不收新任务,跑完存量
shutdownNow()强制:中断运行中的任务,返回未执行的任务列表
awaitTermination(t, unit)阻塞等待关闭完成

九、Spring Boot 中的线程池

java
@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean("bizExecutor")
    public ThreadPoolTaskExecutor bizExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(8);
        executor.setMaxPoolSize(16);
        executor.setQueueCapacity(1000);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("biz-async-");
        executor.setRejectedExecutionHandler(
                new ThreadPoolExecutor.CallerRunsPolicy());

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

        executor.initialize();
        return executor;
    }
}

// 使用
@Async("bizExecutor")                        // ② 指定线程池名,别用默认的
public void sendEmail(String to) { }

⚠️ @Async 不指定线程池名时,Spring Boot 用 SimpleAsyncTaskExecutor——它每次都新建线程,根本不是池

十、监控线程池

java
// 定时上报关键指标
public void monitor(ThreadPoolExecutor pool) {
    log.info("核心={}, 活跃={}, 最大={}, 队列={}/{}, 已完成={}",
            pool.getCorePoolSize(),
            pool.getActiveCount(),          // ① 正在执行任务的线程数
            pool.getMaximumPoolSize(),
            pool.getQueue().size(),         // ② 排队任务数 —— 最重要的指标
            pool.getQueue().size() + pool.getQueue().remainingCapacity(),
            pool.getCompletedTaskCount());
}

告警规则:队列使用率 > 80% 时告警。这说明消费速度跟不上生产速度,再涨就要触发拒绝策略了。

十一、本章小结

要点关键
必须用线程池复用、限流、可管理
提交流程核心 → 队列 → 非核心 → 拒绝
无界队列maxPoolSize 失效,会 OOM
拒绝策略推荐 CallerRunsPolicy
禁用 Executors一律手动 new
线程命名必须,排查问题靠它
@Async必须指定自定义线程池
优雅关闭shutdown + awaitTermination

动手练习

练习 1:基础题

创建一个 core=2, max=4, queue=2 的线程池,提交 10 个任务(每个 sleep 1 秒),观察哪些被执行、哪些被拒绝,验证提交流程。

练习 2:进阶题

实现一个可动态调整参数的线程池:暴露 HTTP 接口,运行时修改 corePoolSizemaximumPoolSize(提示:setCorePoolSize() 方法支持运行时修改)。


下一章第 40 章:JUC 与 JVM

本站基于 VitePress 构建 · 由 Codebook 团队维护