Files
autumn-recruitment/01.Java/concurrent/线程池参数与拒绝策略.md
T

126 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [java/lang, thread-pool, ThreadPoolExecutor, rejection-policy, task-queue]
create time: 2026-08-08 18:00
update time: 2026-08-08 18:00
---
# 线程池参数与拒绝策略
## 概述
线程池是 Java 并发编程的核心工具类,通过复用预先创建的线程来降低频繁创建/销毁线程的开销。ThreadPoolExecutor 是所有线程池工厂(Executors)背后的真正实现,直接暴露了全部可调参数——理解它的运行机制是编写可靠并发代码的第一步。
## 核心原理
### ThreadPoolExecutor 七大参数
```java
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit, // 存活时间单位
BlockingQueue<Runnable> workQueue, // 工作队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
) {}
```
这七个参数中,前五个决定了线程池的**容量模型**和**任务调度逻辑**,后两个属于**扩展点**。
### 任务提交流程
当调用 `execute(Runnable)` 时,任务按以下优先级流转:
```mermaid
flowchart TD
A["提交任务"] --> B{"当前线程数 < corePoolSize?"}
B -->|是| C["创建核心线程执行任务"]
B -->|否| D{"队列是否已满?"}
D -->|否| E["放入工作队列等待"]
D -->|是| F{"当前线程数 < maxPoolSize?"}
F -->|是| G["创建非核心线程执行任务"]
F -->|否| H["执行拒绝策略"]
C --> I[完成]
E --> J{"核心线程<br/>超时检测"}
J -->|未超时| E
J -->|已超时| K["回收线程"]
G --> I
H --> L["记录异常 / 告警"]
```
**流程解读**:
1. **第一步**:如果当前运行线程数小于 `corePoolSize`,即使有空闲线程也创建新线程执行任务(除非设置了 `allowCoreThreadTimeOut`)。
2. **第二步**:如果线程数已达核心数且队列未满,将任务放入工作队列排队。
3. **第三步**:如果队列已满但线程数未达到 `maximumPoolSize`,创建非核心线程处理。
4. **第四步**:如果线程数达到上限且队列仍满,触发拒绝策略。
> [!NOTE]
> 这是面试高频坑点:`Executors.newFixedThreadPool()` 使用的是无界队列 `LinkedBlockingQueue`(capacity = Integer.MAX_VALUE),导致第 3、4 步永远不会发生——所有任务都在队列中堆积,线程池永远只有 corePoolSize 个线程,`maximumPoolSize` 形同虚设。这在流量突增时会导致 OOM。
### 四种拒绝策略
| 策略 | 行为 | 适用场景 |
|------|------|---------|
| `AbortPolicy`(默认) | 抛出 `RejectedExecutionException` | 要求必须处理的场景,快速失败 |
| `CallerRunsPolicy` | 由提交任务的线程直接执行 | 降速缓冲,让提交方承担处理成本 |
| `DiscardPolicy` | 静默丢弃任务 | 可丢失的非关键任务 |
| `DiscardOldestPolicy` | 丢弃队列中最老的任务,再尝试提交 | 保最新数据的批处理场景 |
> [!WARNING]
> `CallerRunsPolicy` 是最"温和"的策略——但它会将任务回退到提交线程(通常是客户请求线程),如果任务耗时较长会阻塞整个业务链路。使用时务必确认调用方的线程性质。
### 工作队列类型
| 队列 | 类型 | 特点 |
|------|------|------|
| `ArrayBlockingQueue` | 有界数组 | FIFO,公平可控,推荐生产使用 |
| `LinkedBlockingQueue` | 无界链表 | 默认 capacity = MAX_VALUE,易导致堆积 |
| `SynchronousQueue` | 同步传输 | 不存储元素,直接交接,配合 `newCachedThreadPool` |
| `PriorityBlockingQueue` | 优先队列 | 支持自定义优先级排序 |
```java
// 推荐的线程池配置示例
ExecutorService pool = new ThreadPoolExecutor(
8, // corePoolSize
16, // maximumPoolSize
60L, TimeUnit.SECONDS, // keepAliveTime
new ArrayBlockingQueue<>(100), // 有界队列,明确上限
new ThreadFactoryBuilder()
.setNameFormat("biz-pool-%d")
.build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 降级而非崩溃
);
```
### CPU 密集型 vs IO 密集型调优公式
线程数的合理设定直接影响吞吐量。经验公式如下:
| 任务类型 | 公式 | 说明 |
|----------|------|------|
| CPU 密集型 | N + 1(N = CPU 核心数) | 多一个线程可以容忍页缺失等偶发停顿 |
| IO 密集型 | N / (1 - 阻塞系数) | 阻塞系数通常取 0.8~0.9,即 N × 5 ~ N × 10 |
| 混合型 | 根据各阶段占比加权平均 | CPU 阶段用 N+1,IO 阶段用大倍数 |
举例:在 8 核机器上处理 IO 密集型任务(阻塞系数 0.85):
```
线程数 ≈ 8 / (1 - 0.85) ≈ 53 个线程
```
> [!TIP]
> 面试常考点:为什么 CPU 密集型只需 N+1?因为 CPU 密集型的线程几乎一直在计算,不需要等待 IO。多余线程只会增加上下文切换开销,不会提升吞吐。
## 实践场景
**常见反模式排查清单**:
1. 用 `Executors.newFixedThreadPool()` 接高并发请求 → 换为有界队列的 `ThreadPoolExecutor`
2. 用 `new CachedThreadPool()` 处理持久化任务 → 缓存池会在空闲 60s 后回收所有线程,反复创建带来额外开销
3. 任务没有设置超时机制 → 配合 `Future.get(timeout)` 或CompletableFuture.orTimeout()
## 关联笔记
- [[01.Java/concurrent/锁升级与 CAS 机制]]