线程池原理:核心参数、工作队列与拒绝策略

线程池这东西,说白了就是「复用线程」的容器。我刚开始接触并发编程时,总觉得每次 new Thread() 挺爽的,直到线上服务被频繁创建线程搞到 OOM……嗯,从那以后我再也不敢手撸线程了。

今天咱们就聊聊线程池的三个核心模块:参数怎么配、队列怎么选、拒绝策略怎么用。这些都是我实际项目中踩过的坑,希望能帮你少走弯路。

一、核心参数:七个参数,一个都不能少

Java 的 ThreadPoolExecutor 有七个参数,我习惯把它们分成三组来看:

参数 作用 我的建议
corePoolSize 核心线程数 根据 CPU 密集型或 IO 密集型来定
maximumPoolSize 最大线程数 一般不超过 corePoolSize 的 2 倍
keepAliveTime 空闲线程存活时间 默认 60 秒,够用
unit 时间单位 通常用 TimeUnit.SECONDS
workQueue 任务队列 重点,下面细说
threadFactory 线程工厂 一定要自定义,方便排查问题
handler 拒绝策略 根据业务场景选

核心逻辑:当提交任务时,如果当前线程数小于 corePoolSize,直接创建新线程执行;如果大于 corePoolSize 且队列未满,则放入队列;如果队列满了且线程数小于 maximumPoolSize,创建临时线程;如果都满了,触发拒绝策略。

我个人习惯把 corePoolSize 设为 CPU 核心数 + 1(CPU 密集型)或 2 * CPU 核心数(IO 密集型)。但别死板,线上环境最好压测一下。

二、工作队列机制:三种队列,各有千秋

队列是线程池的「缓冲层」。我见过不少同学因为队列选错,导致系统崩溃。咱们一个一个看:

1. SynchronousQueue

这个队列不存储任务,它直接把任务交给线程。说白了,就是「一手交任务,一手交线程」。适合任务量不大、但要求快速响应的场景。

我曾经在一个实时计算项目里用过它,配合较大的 maximumPoolSize,效果还不错。但要注意,如果任务提交速度超过处理速度,会频繁创建线程,甚至触发拒绝策略。

2. LinkedBlockingQueue

无界队列,可以一直往里面塞任务。看起来很美,但风险很大——队列可能会无限增长,最终撑爆内存。

避坑指南:我曾经接手过一个遗留系统,线程池用了无界队列,结果某次大促时队列里堆积了上千万个任务,直接导致 Full GC 频繁,服务挂了半小时。从那以后,我再也不敢用无界队列了。

3. ArrayBlockingQueue

有界队列,可以指定容量。这是我最推荐的队列类型。容量设置多少?我一般根据任务处理时间和系统负载来算:

// 假设每个任务处理 100ms,系统能接受的最大延迟是 5 秒
// 那么队列容量 = 5000ms / 100ms = 50
ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(50);

当然,这只是个粗略估算。实际项目中我会留 20% 的余量,再配合监控告警,这样比较稳妥。

三、拒绝策略详解:四种策略,选对很重要

当线程池和队列都满了,新任务怎么办?JDK 提供了四种策略:

策略 行为 适用场景
AbortPolicy 直接抛异常 关键任务,不能丢
CallerRunsPolicy 调用者线程执行 降低任务提交速度
DiscardPolicy 默默丢弃 非重要任务
DiscardOldestPolicy 丢弃最旧的任务 时效性要求高的场景

我的经验:大部分场景用 CallerRunsPolicy 比较安全。它让提交任务的线程自己执行,相当于「反压」机制——提交者慢了,自然就减少了任务提交量。我在日志采集系统里就用这个策略,效果很好。

但要注意,CallerRunsPolicy 可能会阻塞调用线程。如果调用线程是 Web 请求线程,那就要小心了,可能会导致请求超时。

四、实战配置示例

说了这么多,咱们看一个完整的配置示例:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4,                      // corePoolSize
    8,                      // maximumPoolSize
    60,                     // keepAliveTime
    TimeUnit.SECONDS,       // unit
    new ArrayBlockingQueue<>(100),  // workQueue
    new ThreadFactoryBuilder()
        .setNameFormat("my-pool-%d")
        .build(),           // threadFactory
    new ThreadPoolExecutor.CallerRunsPolicy()  // handler
);

这个配置适合大多数 IO 密集型应用。核心线程 4 个,最大 8 个,队列容量 100。当任务量暴增时,先填队列,再扩线程,最后让调用者自己执行。

重要提醒:一定要自定义 ThreadFactory!默认的线程名是 pool-1-thread-1,线上排查问题时根本不知道是哪个线程池。我习惯用 Guava 的 ThreadFactoryBuilder 或者自己实现一个,给线程池起个有意义的名字。

五、监控与调优

线程池配好了不代表万事大吉。我建议至少监控这几个指标:

  • 活跃线程数:是否经常达到 maximumPoolSize
  • 队列积压:队列长度是否持续增长
  • 拒绝次数:触发了多少次拒绝策略
  • 任务执行时间:单个任务的平均耗时

我曾经在一个支付系统中发现,线程池的活跃线程数经常飙到最大值,队列也一直在涨。排查后发现是某个第三方接口响应变慢了,导致任务积压。后来加了超时和熔断,问题就解决了。

嗯,线程池的原理其实不复杂,但用好了能大幅提升系统稳定性。记住一句话:参数要压测,队列要有界,策略要选对,监控不能少

下一章咱们聊聊线程池的源码实现,看看 JDK 到底是怎么管理这些线程的。到时候你会发现,原来线程池的设计思路这么巧妙。