线程池原理与调优 - 并发 | 炼金塔
线程池原理与调优
Section titled “线程池原理与调优”一、引入背景
Section titled “一、引入背景”1.1 为什么要使用线程池?
Section titled “1.1 为什么要使用线程池?”- 性能优化:避免频繁创建/销毁线程的系统开销
- 资源控制:防止大量并发请求创建过多线程导致 OOM
- 任务缓冲:通过队列缓冲任务,平滑处理峰值流量
1.2 实际业务场景痛点
Section titled “1.2 实际业务场景痛点”| 场景 | 痛点 | 解决方案 |
|---|---|---|
| 电商大促 | 瞬时请求激增 | 线程池缓冲 + 队列 |
| 文件处理 | 任务耗时不确定 | 动态调整线程数 |
| 异步消息 | 解耦耗时操作 | 线程池异步执行 |
线程的创建与销毁涉及系统调用,成本较高(分配内核线程、栈空间等)。线程池通过复用已创建的线程,避免频繁创建销毁的开销,同时通过队列缓冲任务,控制并发数量,防止资源耗尽。
没有线程池: 请求 1 → 创建线程 → 执行 → 销毁线程 请求 2 → 创建线程 → 执行 → 销毁线程 问题:创建/销毁开销大;大量请求时可能创建过多线程,OOM
使用线程池: 初始化 → [核心线程 × N 在等待任务] 请求 1 → 复用线程 → 执行 → 线程回到等待状态 请求 2 → 复用线程 → 执行 → 线程回到等待状态 超过容量 → 入队列 → 等待线程空闲ThreadPoolExecutor 七大参数
Section titled “ThreadPoolExecutor 七大参数”public ThreadPoolExecutor( int corePoolSize, // 核心线程数 int maximumPoolSize, // 最大线程数 long keepAliveTime, // 非核心线程空闲存活时间 TimeUnit unit, // keepAliveTime 的时间单位 BlockingQueue<Runnable> workQueue, // 任务队列 ThreadFactory threadFactory, // 线程工厂 RejectedExecutionHandler handler // 拒绝策略)参数含义详解
Section titled “参数含义详解”| 参数 | 含义 | 典型值/注意事项 |
|---|---|---|
corePoolSize | 核心线程数,即使空闲也不销毁(除非设置 allowCoreThreadTimeOut) | CPU 密集型 ≈ CPU 核数;IO 密集型 ≈ 2× CPU 核数 |
maximumPoolSize | 最大线程数,队列满后可创建到此上限 | 需结合业务峰值与机器资源设置 |
keepAliveTime | 非核心线程(超过 corePoolSize 的部分)空闲超过此时间会被销毁 | 通常 60 秒 |
workQueue | 缓冲任务的阻塞队列 | 类型选择影响极大,见下文 |
threadFactory | 创建线程的工厂 | 应设置有意义的线程名,方便排查问题 |
handler | 队列满且线程数达上限时的拒绝策略 | 生产环境不能用 CallerRunsPolicy 阻塞业务线程 |
任务处理流程(核心!)
Section titled “任务处理流程(核心!)”提交任务(execute/submit) ↓当前线程数 < corePoolSize? ├── 是 → 创建核心线程执行任务(即使有空闲线程也会新建!) └── 否 → ↓ 队列未满? ├── 是 → 任务入队等待 └── 否 → ↓ 当前线程数 < maximumPoolSize? ├── 是 → 创建非核心线程执行任务 └── 否 → 执行拒绝策略 (RejectedExecutionHandler)任务队列选型
Section titled “任务队列选型”| 队列类型 | 特性 | 适用场景 | 风险 |
|---|---|---|---|
LinkedBlockingQueue | 无界(Integer.MAX_VALUE) | Executors.newFixedThreadPool 默认 | 队列无限增长,OOM 风险 |
ArrayBlockingQueue | 有界,需指定容量 | 生产环境推荐,可控资源上限 | 配置不当触发拒绝策略 |
SynchronousQueue | 容量为 0,直接交付 | Executors.newCachedThreadPool 默认 | 无缓冲,必须有空闲线程否则直接创建 |
PriorityBlockingQueue | 无界,按优先级排序 | 有任务优先级需求 | 低优先级任务可能饥饿 |
LinkedTransferQueue | 无界,支持直接交付 | 高吞吐场景 | 仍是无界 |
DelayQueue | 无界,延迟到期才取出 | 定时任务、缓存过期 | 时间精度受限于调度精度 |
四种拒绝策略
Section titled “四种拒绝策略”| 策略 | 行为 | 适用场景 |
|---|---|---|
AbortPolicy(默认) | 抛出 RejectedExecutionException | 需要明确感知拒绝的场景 |
CallerRunsPolicy | 由提交任务的线程执行该任务 | 降速保护,但可能阻塞业务线程 |
DiscardPolicy | 静默丢弃新任务 | 允许丢弃不重要的任务 |
DiscardOldestPolicy | 丢弃队列中最旧的任务,再重试提交 | 更关注新任务的场景 |
生产建议:自定义拒绝策略,记录监控日志或将任务持久化,而不是直接丢弃或抛异常。
executor.setRejectedExecutionHandler((r, pool) -> { log.error("任务被拒绝,队列大小={}, 活跃线程={}", pool.getQueue().size(), pool.getActiveCount()); // 可选:将任务写入消息队列等持久化存储 fallbackQueue.offer(r);});Executors 工厂方法的问题
Section titled “Executors 工厂方法的问题”// 危险:无界队列,任务堆积导致 OOMExecutors.newFixedThreadPool(10);// → new ThreadPoolExecutor(10, 10, 0L, MILLISECONDS, new LinkedBlockingQueue<Runnable>())
// 危险:最大线程数 Integer.MAX_VALUE,大量任务导致线程数爆炸 OOMExecutors.newCachedThreadPool();// → new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, SECONDS, new SynchronousQueue<Runnable>())
// 危险:同 newFixedThreadPool,无界队列Executors.newSingleThreadExecutor();线程池参数设置指南
Section titled “线程池参数设置指南”CPU 密集型任务: 最优线程数 ≈ CPU 核心数 + 1 (+1 是为了防止某线程因缺页故障等偶发原因导致 CPU 空闲)
IO 密集型任务: 最优线程数 ≈ CPU 核心数 × (1 + IO等待时间 / CPU计算时间) 简化估算:CPU 核心数 × 2(I/O 等待占多数时)
混合型任务: 尽量拆分为 CPU 密集和 IO 密集两个线程池分别处理纯理论公式在实际中往往不够用,原因是:
- 业务任务的 IO 等待时间随负载动态变化
- 机器上运行的不只是这一个线程池
- 不同 QPS 下最优参数不同
正确做法:
- 基于理论公式给出初始值
- 压测,观察 CPU 利用率、任务等待时间、队列积压情况
- 动态可配置化(通过配置中心实时调整,不重启服务)
// 支持动态修改的线程池(可接入配置中心)executor.setCorePoolSize(newCoreSize);executor.setMaximumPoolSize(newMaxSize);// 注意:setCorePoolSize < 当前线程数时,不会立即终止线程// setMaximumPoolSize 必须 >= corePoolSizeForkJoinPool 与工作窃取
Section titled “ForkJoinPool 与工作窃取”ForkJoinPool 专为分治算法(Divide and Conquer)设计,Java 8 parallelStream 和 CompletableFuture 的默认异步池均使用它。
工作窃取(Work Stealing)
Section titled “工作窃取(Work Stealing)”ForkJoinPool 结构:
Worker-0: [task0, task1, task2] ← 自己从队尾取任务(LIFO)Worker-1: [task3, task4]Worker-2: [] ← 空闲!从 Worker-0 的队头窃取 task0(FIFO)
双端队列设计的巧妙之处: • 自己从队尾取(减少与窃取者的竞争) • 窃取者从队头取(通常是更大的父任务,窃取后能继续分裂出更多子任务)RecursiveTask 示例
Section titled “RecursiveTask 示例”// 并行计算数组求和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));线程池监控指标
Section titled “线程池监控指标”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 → 立即告警二、面试突击篇
Section titled “二、面试突击篇”2.1 原理分析题
Section titled “2.1 原理分析题”Q1: 线程池的任务提交流程是怎样的?
🎯 考察重点: 线程池任务处理机制
📝 回答要点:
- 线程数 < corePoolSize → 创建核心线程执行
- 线程数 ≥ corePoolSize → 尝试加入任务队列
- 队列满 → 线程数 < maximumPoolSize → 创建非核心线程
- 队列满且线程数达上限 → 执行拒绝策略
💡 记忆技巧: “先核心,后队列,再扩容,最后拒”
🔍 追问扩展:
- Q: 为什么要先创建核心线程再入队? A: 核心线程是预创建的,能更快响应任务,避免任务堆积。
2.2 实战应用题
Section titled “2.2 实战应用题”Q2: 为什么不建议用 Executors 工厂方法?
🎯 考察重点: 线程池参数选择、OOM 风险
📝 回答要点:
| 工厂方法 | 问题 | 风险 |
|---|---|---|
| newFixedThreadPool | 无界 LinkedBlockingQueue | 任务堆积 OOM |
| newCachedThreadPool | 最大线程数 Integer.MAX_VALUE | 线程数爆炸 OOM |
| newSingleThreadExecutor | 同 Fixed,无隔离 | 单点瓶颈 |
正确做法:使用 ThreadPoolExecutor 明确指定参数。
🔍 追问扩展:
- Q: 如何设置合理的线程池参数? A: CPU 密集型 ≈ CPU + 1;IO 密集型 ≈ CPU × 2;压测调优。
3.1 核心要点回顾
Section titled “3.1 核心要点回顾”| 知识点 | 关键点 | 面试权重 |
|---|---|---|
| 七大参数 | core/max/queue/keepAliveTime | ★★★★★ |
| 任务流程 | 先核心→后队列→再扩容→拒绝 | ★★★★★ |
| 队列选型 | Array vs Linked vs Synchronous | ★★★★☆ |
| 拒绝策略 | Abort/CallerRuns/Discard | ★★★☆☆ |
3.2 学习建议
Section titled “3.2 学习建议”- 掌握流程图:任务提交流程要能默写
- 理解参数含义:每个参数的作用和设置原则
- 避免 Executors:直接使用 ThreadPoolExecutor
- 监控调优:根据监控数据动态调整
3.3 扩展阅读
Section titled “3.3 扩展阅读”- JDK 源码:
ThreadPoolExecutor.java - 阿里巴巴 Java 开发手册:并发处理章节
- 美团技术文章:Java 线程池实现原理与调优