跳到主要内容

线程池详解

线程池(ThreadPoolExecutor)通过复用线程、队列化任务、限制并发度来提升吞吐并避免频繁创建线程的开销,是服务端并发的“必备基础设施”。


为什么需要线程池?

  • 创建销毁成本高:频繁 new Thread() 会触发内核调度与内存分配。
  • 资源可控:线程数不可无限增长,否则会触发 OOM 或导致上下文切换风暴。
  • 任务管理:线程池能统一应用链路的超时、降级、监控、优雅关闭。
  • 异步编排:配合 Future, CompletableFuture 构建异步流水线。

ThreadPoolExecutor 核心参数

public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数作用设计建议
corePoolSize常驻线程数IO 密集型可设置为 CPU * 2,或按压测结果调优
maximumPoolSize最大线程数防止突发流量压垮系统,可配合熔断限流
workQueue任务队列有界队列可提供背压;SynchronousQueue 适合直接移交
keepAliveTime非核心线程闲置回收时间IO 场景可略高,CPU 场景可更短
threadFactory设定线程名、优先级、是否守护线程便于排查,推荐自定义命名
handler拒绝策略结合业务场景选择或自定义

常用队列:LinkedBlockingQueue(默认无界,谨慎)、ArrayBlockingQueue(有界)、SynchronousQueue(直通,高并发下触发扩容)。


任务调度流程

  1. 线程数 < corePoolSize:直接创建新线程执行任务;
  2. 否则尝试入队 workQueue
  3. 队列满且线程数 < maximumPoolSize:再创建非核心线程;
  4. 再次失败则触发拒绝策略。

拒绝策略

  • AbortPolicy:抛 RejectedExecutionException,默认策略。
  • CallerRunsPolicy:调用者线程直接执行任务,提供自然的背压。
  • DiscardPolicy:悄然丢弃,需谨慎。
  • DiscardOldestPolicy:丢弃队头任务再尝试入队,可能导致任务饥饿。
  • 自定义策略:记录日志、降级响应、写入 MQ 重试等。

Executors 快捷工厂的坑

工厂方法问题建议替代
newFixedThreadPool使用无界 LinkedBlockingQueue,任务堆积易 OOM显式 new ThreadPoolExecutor
newCachedThreadPool最大线程数 Integer.MAX_VALUE,高峰可能撑爆设定合理 maximumPoolSize
newScheduledThreadPool核心线程无上限回收,提交过多一次性任务会堆积JDK9+ 用 ScheduledThreadPoolExecutor + 自定义队列
newSingleThreadExecutor单线程一旦被阻塞,所有任务等待必要时用 ThreadPoolExecutor + 有界队列

面试常问:“为什么不建议直接使用 Executors?”——答:参数不可控,隐藏 OOM 风险。


自定义线程池示例

ThreadPoolExecutor bizPool = new ThreadPoolExecutor(
8, // core
16, // max
60, TimeUnit.SECONDS, // keepAlive
new ArrayBlockingQueue<>(2000), // 有界队列
new ThreadFactoryBuilder()
.setNameFormat("biz-worker-%d")
.setUncaughtExceptionHandler((t, e) -> log.error("线程崩溃", e))
.build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 背压
);

// 优雅关闭
bizPool.shutdown();
if (!bizPool.awaitTermination(30, TimeUnit.SECONDS)) {
bizPool.shutdownNow();
}

监控与调优指标

  • 线程池运行状态getPoolSize, getActiveCount, getTaskCount, getCompletedTaskCount
  • 队列长度workQueue.size(),是观察积压的关键指标。
  • 拒绝次数:自定义 RejectedExecutionHandler 统计。
  • 任务耗时:在提交任务前后打点或使用 TimingThreadPool 装饰器。
  • 链路超时:线程池只是执行体,仍需配合业务超时控制(Future#get(timeout))。

线程池拆分策略

  • 按业务隔离:接口调用、异步 MQ、批任务各用一个线程池,避免互相拖垮。
  • 按 SLA 级别:核心链路线程池优先保留资源,低优任务可使用更小的池或降级。
  • 按请求类型:CPU 密集型与 IO 密集型配置不同,避免混用。
  • 按租户/用户:多租户系统可考虑限流 + 线程池隔离。

常见问题排查

  • CPU 打满:检查是否提交了大量 CPU 计算任务,可通过 jstack 看活跃线程栈帧。
  • 任务堆积:观察队列长度与 activeCount,必要时扩容或开启降级逻辑。
  • 线程泄漏:忘记调用 shutdown,或线程池被频繁创建导致占满资源。
  • 拒绝策略频繁触发:说明系统已超容量,需扩容、限流或快速失败。

高频面试题

  1. 如何估算线程池大小?
    CPU 密集型:CPU 核心数 + 1;IO 密集型:CPU 核心数 * (1 + 平均等待时间/平均计算时间);最终以压测数据为准。

  2. 如何实现优先级线程池?
    使用 PriorityBlockingQueue 作为 workQueue,任务实现 Comparable 或自定义 Comparator

  3. 如何动态调整线程池参数?
    调用 setCorePoolSize, setMaximumPoolSize, setKeepAliveTime 即可,可结合配置中心热更新。

  4. shutdown()shutdownNow() 区别?
    shutdown() 进入平滑关闭状态,不再接收新任务但会完成已提交任务;shutdownNow() 尝试中断正在执行的任务并清空队列。

  5. 如何避免线程池中的任务丢失?

    • 任务入队失败时记录日志 / 写入补偿队列
    • 使用可靠消息(MQ)或持久化
    • 应用关闭前 awaitTermination,确保处理完成

最佳实践清单

  • 明确线程池用途并按业务拆分。
  • 选择有界队列并评估容量。
  • 自定义 ThreadFactory,设置易读的线程名。
  • 结合监控平台暴露线程池指标。
  • 制定拒绝策略:重试、降级或快速失败。
  • 定期复盘压测数据,动态调参。

小结

  • 线程池核心在于“控制与复用”:控制线程数量、任务堆积与失败策略,复用线程提升吞吐。
  • 切勿直接使用 Executors 默认工厂,显式构造可读、可控的 ThreadPoolExecutor
  • 监控 + 限流 + 降级是线程池稳定运行的三件套。