Java 多线程与线程池完整笔记
一、线程池基础
1.1 什么是线程池
线程池是一种多线程处理模式,它在系统启动时预先创建一定数量的线程放入池中。这些线程在没有任务时处于等待状态,当有任务提交时,池中某个空闲线程会被唤醒执行任务。任务执行完成后,线程并不会被销毁,而是返回池中继续等待下一个任务。
┌─────────────────────────────────────────────────┐
│ 线程池架构 │
├─────────────────────────────────────────────────┤
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │线程1│ │线程2│ │线程3│ │线程N│ ← 常驻线程 │
│ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ │
│ │ │ │ │ │
│ └─────────┴────┬────┴─────────┘ │
│ ▼ │
│ ┌───────────────┐ │
│ │ 任务队列 │ ← 缓冲待处理任务 │
│ └───────────────┘ │
└─────────────────────────────────────────────────┘
1.2 为什么需要线程池
| 优势 | 说明 |
|---|
| 资源复用 | 避免频繁创建和销毁线程的开销,线程对象可重复使用 |
| 响应速度 | 任务到达时无需等待线程创建,立即执行 |
| 可管理性 | 统一分配、调优和监控线程,防止资源耗尽 |
| 流量控制 | 通过队列和最大线程数限制并发量,保护系统 |
二、ThreadPoolExecutor 核心参数详解
2.1 七个核心参数
public ThreadPoolExecutor(
int corePoolSize, // 1. 核心线程数
int maximumPoolSize, // 2. 最大线程数
long keepAliveTime, // 3. 空闲线程存活时间
TimeUnit unit, // 4. 时间单位
BlockingQueue<Runnable> workQueue, // 5. 工作队列
ThreadFactory threadFactory, // 6. 线程工厂
RejectedExecutionHandler handler // 7. 拒绝策略
)
| 参数 | 含义 | 比喻 |
|---|
corePoolSize | 核心线程数,即使空闲也保留在池中的线程数量 | 餐厅正式员工,永不辞退 |
maximumPoolSize | 最大线程数 = 核心线程 + 临时线程 | 正式员工 + 临时工的上限 |
keepAliveTime | 临时线程空闲超过该时间则被回收 | 临时工多久没活干就辞退 |
unit | keepAliveTime 的时间单位 | 秒、毫秒等 |
workQueue | 存放待执行任务的阻塞队列 | 餐厅等位区 |
threadFactory | 创建线程的工厂,可自定义线程名、优先级等 | 员工招聘标准 |
handler | 队列满且线程达最大值时的拒绝策略 | 客满时的处理方式 |
2.2 常用工作队列类型
| 队列类型 | 特点 | 适用场景 |
|---|
SynchronousQueue | 不存储任务,直接交给线程执行 | CachedThreadPool |
LinkedBlockingQueue | 无界队列(默认 Integer.MAX_VALUE) | FixedThreadPool |
ArrayBlockingQueue | 有界队列,需指定容量 | 需限制队列长度的场景 |
PriorityBlockingQueue | 优先级队列 | 任务有优先级要求 |
三、线程池执行流程(五步核心逻辑)
// 伪代码表示执行逻辑
public void execute(Runnable task) {
// 步骤1:当前线程数 < corePoolSize
if (workerCount < corePoolSize) {
addWorker(task, true); // 新建核心线程执行
return;
}
// 步骤2:核心线程已满,尝试放入队列
if (workQueue.offer(task)) {
// 放入成功,等待空闲线程取走执行
return;
}
// 步骤3:队列已满,尝试创建临时线程
if (workerCount < maximumPoolSize) {
addWorker(task, false); // 新建临时线程执行
return;
}
// 步骤4:线程数已达最大值,执行拒绝策略
reject(task);
}
流程图:
提交任务
│
▼
当前线程数 < corePoolSize? ──Yes──▶ 创建核心线程执行
│
No
│
▼
任务能否入队? ──Yes──▶ 入队等待执行
│
No(队列已满)
│
▼
当前线程数 < maximumPoolSize? ──Yes──▶ 创建临时线程执行
│
No
│
▼
执行拒绝策略
四、四种拒绝策略
| 策略类 | 行为 | 比喻 | 使用建议 |
|---|
AbortPolicy | 抛出 RejectedExecutionException | 直接赶走客人并吵架 | 默认策略,开发常用,快速失败 |
CallerRunsPolicy | 由提交任务的线程自己执行 | 老板亲自服务客人 | 可减缓提交速度,重要任务兜底 |
DiscardPolicy | 静默丢弃新任务 | 悄悄赶走客人 | 不重要的任务,如日志上报 |
DiscardOldestPolicy | 丢弃队列中最老的任务 | 赶走排队最久的人 | 优先处理新任务 |
// 自定义拒绝策略示例
new ThreadPoolExecutor.AbortPolicy() // 抛异常
new ThreadPoolExecutor.CallerRunsPolicy() // 调用者执行
new ThreadPoolExecutor.DiscardPolicy() // 直接丢弃
new ThreadPoolExecutor.DiscardOldestPolicy() // 丢弃最旧的
五、Executors 四种快捷线程池
⚠️ 开发规范:强制禁止使用 Executors 创建线程池,必须通过 ThreadPoolExecutor 构造函数显式指定参数。
原因:
- FixedThreadPool 和 SingleThreadPool 使用无界队列,可能 OOM
- CachedThreadPool 和 ScheduledThreadPool 允许创建无限数量线程,可能 OOM
| 工厂方法 | 核心特点 | 适用场景 | 风险 |
|---|
newFixedThreadPool(n) | 固定线程数,无界队列 | 重负载服务器 | 队列无限堆积 |
newCachedThreadPool() | 0核心,无限临时线程,同步队列 | 大量短期异步任务 | 线程数爆炸 |
newSingleThreadExecutor() | 单线程,无界队列 | 任务需顺序执行 | 队列无限堆积 |
newScheduledThreadPool(n) | 支持定时/周期任务 | 定时调度 | 线程数爆炸 |
// ❌ 错误示范
ExecutorService pool = Executors.newFixedThreadPool(10);
// ✅ 正确示范
ExecutorService pool = new ThreadPoolExecutor(
10, 20, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("my-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
六、线程数设置公式
6.1 CPU 密集型任务
定义:大量时间用于计算,很少等待 I/O
线程数 = CPU 核心数 + 1
加 1 是为了在某个线程因缺页中断等原因暂停时,有备用线程顶上,最大化 CPU 利用率。
6.2 I/O 密集型任务
定义:大量时间等待 I/O 操作(网络、磁盘、数据库)
线程数 = CPU 核心数 × 2
更精确的公式:
线程数 = CPU 核心数 × (1 + 平均等待时间 / 平均计算时间)
6.3 混合型任务
将任务拆分为 CPU 密集和 I/O 密集两部分,分别用不同线程池处理。
七、线程池生命周期与优雅关闭
7.1 五种状态
| 状态 | 说明 | 接收新任务 | 执行队列任务 |
|---|
RUNNING | 正常运行 | ✅ | ✅ |
SHUTDOWN | 关闭中(调用 shutdown()) | ❌ | ✅ |
STOP | 强制停止(调用 shutdownNow()) | ❌ | ❌ |
TIDYING | 过渡状态,所有任务终止 | ❌ | ❌ |
TERMINATED | 完全终止 | ❌ | ❌ |
7.2 优雅关闭代码模板
public void shutdownGracefully(ExecutorService pool) {
pool.shutdown(); // ① 停止接收新任务
try {
// ② 等待现有任务完成,最多等待 60 秒
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow(); // ③ 超时则强制关闭
// ④ 再等待 5 秒,确认是否终止
if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
System.err.println("线程池未能正常终止");
}
}
} catch (InterruptedException e) {
pool.shutdownNow(); // ⑤ 当前线程被中断,也强制关闭
Thread.currentThread().interrupt();
}
}
八、ScheduledThreadPoolExecutor 定时任务线程池
8.1 概述
ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor,专门用于执行定时和周期性任务,是传统 Timer 的替代品。
| 对比项 | Timer | ScheduledThreadPoolExecutor |
|---|
| 线程模型 | 单线程 | 多线程池 |
| 异常处理 | 任务抛异常则 Timer 线程终止 | 单任务异常不影响其他任务 |
| 任务调度 | 基于绝对时间 | 基于相对时间,更精准 |
| 适用场景 | 简单定时 | 生产级定时调度 |
8.2 三种核心 API
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5);
// 1. 延迟执行一次
ScheduledFuture<?> future = scheduler.schedule(
() -> System.out.println("延迟执行"),
5, TimeUnit.SECONDS
);
// 2. 固定频率执行(不管上次是否完成)
scheduler.scheduleAtFixedRate(
() -> System.out.println("固定频率"),
1, // 首次延迟 1 秒
3, // 每 3 秒执行一次
TimeUnit.SECONDS
);
// 3. 固定延迟执行(等上次完成后再延迟)
scheduler.scheduleWithFixedDelay(
() -> System.out.println("固定延迟"),
1, // 首次延迟 1 秒
3, // 上次完成后等待 3 秒
TimeUnit.SECONDS
);
8.3 scheduleAtFixedRate vs scheduleWithFixedDelay
| 方法 | 调度方式 | 效果 |
|---|
scheduleAtFixedRate | 固定频率(周期固定) | 不管任务执行多久,严格按周期触发,可能任务堆叠 |
scheduleWithFixedDelay | 固定延迟(间隔固定) | 等上次执行完,再等固定时间才触发,周期 = 执行时间 + 延迟 |
scheduleAtFixedRate (周期 = 3秒)
任务执行2秒 → 等待1秒 → 下一轮
任务执行4秒 → 立即触发下一轮(无等待)
scheduleWithFixedDelay (延迟 = 3秒)
任务执行2秒 → 等待3秒 → 下一轮(间隔5秒)
任务执行4秒 → 等待3秒 → 下一轮(间隔7秒)
九、线程池性能监控与调优
9.1 关键监控指标
| 指标 | 含义 | 告警阈值 | 排查方向 |
|---|
| 活跃线程数 | 当前正在工作的线程数 | > 最大线程数 80% | 线程数不足或任务耗时过长 |
| 队列长度 | 等待执行的任务数 | > 队列容量 70% | 消费速度跟不上生产速度 |
| 任务完成数 | 已完成任务累计 | 增长停滞 | 线程池可能卡死或阻塞 |
| 拒绝任务数 | 被拒绝的任务累计 | > 0(正常应为 0) | 线程数和队列容量需调大 |
| 线程池大小 | 当前池中线程总数 | 长期接近 maximumPoolSize | 调大 maximumPoolSize |
| 任务等待时间 | 任务从提交到执行的时间 | > 业务可接受阈值 | 处理能力不足 |
9.2 监控实现方案
@Component
public class ThreadPoolMonitor {
private final ThreadPoolExecutor executor;
@Scheduled(fixedDelay = 30000) // 每30秒采集一次
public void monitor() {
int activeCount = executor.getActiveCount();
long completedCount = executor.getCompletedTaskCount();
int queueSize = executor.getQueue().size();
int poolSize = executor.getPoolSize();
long rejectCount = // 自定义记录
log.info("线程池监控 - 活跃:{}, 队列:{}, 池大小:{}, 已完成:{}, 拒绝:{}",
activeCount, queueSize, poolSize, completedCount, rejectCount);
// 告警判断
if (queueSize > queueCapacity * 0.7) {
alert("队列使用率超过70%");
}
}
}
9.3 调优策略
| 现象 | 可能原因 | 解决方案 |
|---|
| 队列持续增长 | 消费速度 < 生产速度 | 增加线程数,或优化任务执行效率 |
| 活跃线程数持续满 | 任务耗时过长 | 拆分任务、异步化、增加最大线程数 |
| 拒绝次数频繁 | 线程池容量不足 | 增大队列容量和最大线程数 |
| CPU 使用率低但队列满 | 线程阻塞于 I/O | 增加线程数(I/O 密集型公式) |
| OOM 内存溢出 | 无界队列堆积 | 改用有界队列 + 合理拒绝策略 |
十、最佳实践总结
10.1 编码规范
// ✅ 正确:显式创建 ThreadPoolExecutor
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, // 核心线程数
20, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲超时
new ArrayBlockingQueue<>(1000), // 有界队列
new NamedThreadFactory("biz-pool"), // 自定义线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// ❌ 错误:使用 Executors 快捷方法
ExecutorService pool = Executors.newFixedThreadPool(10);
10.2 必须设置的内容
- [ ] 有界队列(避免 OOM)
- [ ] 合适的拒绝策略(根据业务选型)
- [ ] 有意义的线程名称(便于排查问题)
- [ ] 监控指标采集(JMX / Micrometer)
- [ ] 优雅关闭逻辑(应用停止时调用)
10.3 参数选择参考
| 业务类型 | corePoolSize | maximumPoolSize | 队列类型 | 队列容量 |
|---|
| 计算密集型 API | CPU 核数 + 1 | CPU 核数 + 1 | ArrayBlockingQueue | 100~500 |
| I/O 密集型 API | CPU 核数 × 2 | CPU 核数 × 4 | ArrayBlockingQueue | 500~2000 |
| 后台批量处理 | 1~5 | 1~10 | LinkedBlockingQueue | 10000+ |
| 即时响应任务 | 10~50 | 50~200 | SynchronousQueue | 0 |
本笔记涵盖了 Java 多线程线程池的核心知识点,建议结合实战加深理解。
评论