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

126 lines
5.4 KiB
Markdown
Raw Normal View History

2026-08-08 19:01:04 +08:00
---
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 机制]]