Skip to content

线程池原理与调优 - 并发 | 炼金塔


  • 性能优化:避免频繁创建/销毁线程的系统开销
  • 资源控制:防止大量并发请求创建过多线程导致 OOM
  • 任务缓冲:通过队列缓冲任务,平滑处理峰值流量
场景痛点解决方案
电商大促瞬时请求激增线程池缓冲 + 队列
文件处理任务耗时不确定动态调整线程数
异步消息解耦耗时操作线程池异步执行

线程的创建与销毁涉及系统调用,成本较高(分配内核线程、栈空间等)。线程池通过复用已创建的线程,避免频繁创建销毁的开销,同时通过队列缓冲任务,控制并发数量,防止资源耗尽。

没有线程池:
请求 1 → 创建线程 → 执行 → 销毁线程
请求 2 → 创建线程 → 执行 → 销毁线程
问题:创建/销毁开销大;大量请求时可能创建过多线程,OOM
使用线程池:
初始化 → [核心线程 × N 在等待任务]
请求 1 → 复用线程 → 执行 → 线程回到等待状态
请求 2 → 复用线程 → 执行 → 线程回到等待状态
超过容量 → 入队列 → 等待线程空闲

public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit, // keepAliveTime 的时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
参数含义典型值/注意事项
corePoolSize核心线程数,即使空闲也不销毁(除非设置 allowCoreThreadTimeOut)CPU 密集型 ≈ CPU 核数;IO 密集型 ≈ 2× CPU 核数
maximumPoolSize最大线程数,队列满后可创建到此上限需结合业务峰值与机器资源设置
keepAliveTime非核心线程(超过 corePoolSize 的部分)空闲超过此时间会被销毁通常 60 秒
workQueue缓冲任务的阻塞队列类型选择影响极大,见下文
threadFactory创建线程的工厂应设置有意义的线程名,方便排查问题
handler队列满且线程数达上限时的拒绝策略生产环境不能用 CallerRunsPolicy 阻塞业务线程

提交任务(execute/submit)
当前线程数 < corePoolSize?
├── 是 → 创建核心线程执行任务(即使有空闲线程也会新建!)
└── 否 → ↓
队列未满?
├── 是 → 任务入队等待
└── 否 → ↓
当前线程数 < maximumPoolSize?
├── 是 → 创建非核心线程执行任务
└── 否 → 执行拒绝策略 (RejectedExecutionHandler)

队列类型特性适用场景风险
LinkedBlockingQueue无界(Integer.MAX_VALUE)Executors.newFixedThreadPool 默认队列无限增长,OOM 风险
ArrayBlockingQueue有界,需指定容量生产环境推荐,可控资源上限配置不当触发拒绝策略
SynchronousQueue容量为 0,直接交付Executors.newCachedThreadPool 默认无缓冲,必须有空闲线程否则直接创建
PriorityBlockingQueue无界,按优先级排序有任务优先级需求低优先级任务可能饥饿
LinkedTransferQueue无界,支持直接交付高吞吐场景仍是无界
DelayQueue无界,延迟到期才取出定时任务、缓存过期时间精度受限于调度精度

策略行为适用场景
AbortPolicy(默认)抛出 RejectedExecutionException需要明确感知拒绝的场景
CallerRunsPolicy由提交任务的线程执行该任务降速保护,但可能阻塞业务线程
DiscardPolicy静默丢弃新任务允许丢弃不重要的任务
DiscardOldestPolicy丢弃队列中最旧的任务,再重试提交更关注新任务的场景

生产建议:自定义拒绝策略,记录监控日志或将任务持久化,而不是直接丢弃或抛异常。

executor.setRejectedExecutionHandler((r, pool) -> {
log.error("任务被拒绝,队列大小={}, 活跃线程={}",
pool.getQueue().size(), pool.getActiveCount());
// 可选:将任务写入消息队列等持久化存储
fallbackQueue.offer(r);
});

// 危险:无界队列,任务堆积导致 OOM
Executors.newFixedThreadPool(10);
// → new ThreadPoolExecutor(10, 10, 0L, MILLISECONDS, new LinkedBlockingQueue<Runnable>())
// 危险:最大线程数 Integer.MAX_VALUE,大量任务导致线程数爆炸 OOM
Executors.newCachedThreadPool();
// → new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, SECONDS, new SynchronousQueue<Runnable>())
// 危险:同 newFixedThreadPool,无界队列
Executors.newSingleThreadExecutor();

CPU 密集型任务:
最优线程数 ≈ CPU 核心数 + 1
(+1 是为了防止某线程因缺页故障等偶发原因导致 CPU 空闲)
IO 密集型任务:
最优线程数 ≈ CPU 核心数 × (1 + IO等待时间 / CPU计算时间)
简化估算:CPU 核心数 × 2(I/O 等待占多数时)
混合型任务:
尽量拆分为 CPU 密集和 IO 密集两个线程池分别处理

纯理论公式在实际中往往不够用,原因是:

  1. 业务任务的 IO 等待时间随负载动态变化
  2. 机器上运行的不只是这一个线程池
  3. 不同 QPS 下最优参数不同

正确做法

  1. 基于理论公式给出初始值
  2. 压测,观察 CPU 利用率、任务等待时间、队列积压情况
  3. 动态可配置化(通过配置中心实时调整,不重启服务)
// 支持动态修改的线程池(可接入配置中心)
executor.setCorePoolSize(newCoreSize);
executor.setMaximumPoolSize(newMaxSize);
// 注意:setCorePoolSize < 当前线程数时,不会立即终止线程
// setMaximumPoolSize 必须 >= corePoolSize

ForkJoinPool 专为分治算法(Divide and Conquer)设计,Java 8 parallelStreamCompletableFuture 的默认异步池均使用它。

ForkJoinPool 结构:
Worker-0: [task0, task1, task2] ← 自己从队尾取任务(LIFO)
Worker-1: [task3, task4]
Worker-2: [] ← 空闲!从 Worker-0 的队头窃取 task0(FIFO)
双端队列设计的巧妙之处:
• 自己从队尾取(减少与窃取者的竞争)
• 窃取者从队头取(通常是更大的父任务,窃取后能继续分裂出更多子任务)
// 并行计算数组求和
class SumTask extends RecursiveTask<Long> {
private final int[] arr;
private final int from, to;
private static final int THRESHOLD = 1000;
@Override
protected Long compute() {
if (to - from <= THRESHOLD) {
// 直接计算(叶子节点)
long sum = 0;
for (int i = from; i < to; i++) sum += arr[i];
return sum;
}
int mid = (from + to) / 2;
SumTask left = new SumTask(arr, from, mid);
SumTask right = new SumTask(arr, mid, to);
left.fork(); // 异步提交左子任务
return right.compute() // 当前线程执行右子任务
+ left.join(); // 等待左子任务结果
}
}
ForkJoinPool pool = ForkJoinPool.commonPool();
Long result = pool.invoke(new SumTask(arr, 0, arr.length));

ThreadPoolExecutor executor = ...;
// 关键指标
executor.getCorePoolSize() // 核心线程数
executor.getMaximumPoolSize() // 最大线程数
executor.getPoolSize() // 当前线程数
executor.getActiveCount() // 活跃(执行中)线程数
executor.getQueue().size() // 队列中等待的任务数
executor.getTaskCount() // 历史提交总任务数
executor.getCompletedTaskCount() // 历史完成总任务数
// 告警阈值建议
队列积压率 = queue.size() / queue.capacity() > 80% → 告警
线程利用率 = activeCount / maximumPoolSize > 90% → 告警
任务拒绝数 > 0 → 立即告警

Q1: 线程池的任务提交流程是怎样的?

🎯 考察重点: 线程池任务处理机制

📝 回答要点:

  1. 线程数 < corePoolSize → 创建核心线程执行
  2. 线程数 ≥ corePoolSize → 尝试加入任务队列
  3. 队列满 → 线程数 < maximumPoolSize → 创建非核心线程
  4. 队列满且线程数达上限 → 执行拒绝策略

💡 记忆技巧: “先核心,后队列,再扩容,最后拒”

🔍 追问扩展:

  • Q: 为什么要先创建核心线程再入队? A: 核心线程是预创建的,能更快响应任务,避免任务堆积。

Q2: 为什么不建议用 Executors 工厂方法?

🎯 考察重点: 线程池参数选择、OOM 风险

📝 回答要点:

工厂方法问题风险
newFixedThreadPool无界 LinkedBlockingQueue任务堆积 OOM
newCachedThreadPool最大线程数 Integer.MAX_VALUE线程数爆炸 OOM
newSingleThreadExecutor同 Fixed,无隔离单点瓶颈

正确做法:使用 ThreadPoolExecutor 明确指定参数。

🔍 追问扩展:

  • Q: 如何设置合理的线程池参数? A: CPU 密集型 ≈ CPU + 1;IO 密集型 ≈ CPU × 2;压测调优。

知识点关键点面试权重
七大参数core/max/queue/keepAliveTime★★★★★
任务流程先核心→后队列→再扩容→拒绝★★★★★
队列选型Array vs Linked vs Synchronous★★★★☆
拒绝策略Abort/CallerRuns/Discard★★★☆☆
  1. 掌握流程图:任务提交流程要能默写
  2. 理解参数含义:每个参数的作用和设置原则
  3. 避免 Executors:直接使用 ThreadPoolExecutor
  4. 监控调优:根据监控数据动态调整
  • JDK 源码:ThreadPoolExecutor.java
  • 阿里巴巴 Java 开发手册:并发处理章节
  • 美团技术文章:Java 线程池实现原理与调优