以下是 Day 2 详细学习内容(线程池任务队列与拒绝策略实战,30 分钟完整计划),包含理论对比、分步代码实战和现象解析:
今日学习目标
- 握有界队列(ArrayBlockingQueue)与无界队列(LinkedBlockingQueue)的核心区别
- 理解 4 种拒绝策略的适用场景
- 实战:对比不同队列类型下线程池的行为差异
⏰ 时间分配
时间段 |
任务 |
详细内容 |
0-8 分钟 |
理论:队列类型与拒绝策略 |
1. 有界 vs 无界队列的内存安全问题 2. 4 种拒绝策略的使用场景对比 3. 生产环境队列容量设置原则 |
8-25 分钟 |
实战:双队列对比实验 |
1. 编写有界队列线程池(容量 1)2. 编写无界队列线程池 3. 提交过量任务,观察日志差异 |
25-30 分钟 |
总结与扩展 |
1. 记录两种队列的关键区别 2. 思考:为什么无界队列不会触发maximumPoolSize? 3. 扩展:如何选择合适的拒绝策略? |
理论详解:队列类型与拒绝策略
- 有界队列 vs 无界队列
特性 |
有界队列(如ArrayBlockingQueue) |
无界队列(如LinkedBlockingQueue) |
队列容量 |
必须指定容量(如new ArrayBlockingQueue<>(100)) |
容量默认Integer.MAX_VALUE(理论无限) |
线程扩容 |
队列满后创建非核心线程(直到maximumPoolSize) |
队列永不满,不会触发maximumPoolSize |
内存风险 |
安全(容量可控) |
可能 OOM(任务堆积导致内存溢出) |
适用场景 |
流量可预测场景(如订单处理) |
流量突发场景(如秒杀瞬时流量) |
- 4 种拒绝策略对比
策略 |
类名 |
行为描述 |
适用场景 |
抛异常 |
AbortPolicy |
直接抛出RejectedExecutionException |
需要严格感知任务失败的场景 |
静默丢弃 |
DiscardPolicy |
丢弃最新任务,无任何提示 |
非关键任务(如日志上报) |
丢弃最老任务 |
DiscardOldestPolicy |
丢弃队列中等待最久的任务,尝试处理新任务 |
希望处理最新任务的场景 |
调用者执行 |
CallerRunsPolicy |
由提交任务的线程直接执行任务 |
减缓任务提交速度(保护线程池) |
实战步骤:双队列对比实验
import java.util.concurrent.*;
public class BoundedQueueDemo {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(
1,
2,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1),
new ThreadPoolExecutor.AbortPolicy()
);
for (int i = 0; i < 4; i++) {
int taskId = i;
pool.execute(() -> {
try {
System.out.println("任务" + taskId + "由" + Thread.currentThread().getName() + "处理");
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
pool.shutdown();
}
}
任务0由pool-1-thread-1处理 (核心线程)
任务1进入队列
任务2创建非核心线程pool-1-thread-2处理
任务3提交时触发AbortPolicy,抛出RejectedExecutionException
实验 2:无界队列(不触发拒绝策略)
java
public class UnboundedQueueDemo {
public static void main(String[] args) {
// 无界队列(默认容量无限),最大线程2(但队列不会满,故非核心线程不会创建)
ExecutorService pool = new ThreadPoolExecutor(
1, // 核心线程1
2, // 最大线程2(无效,因队列无界)
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), // 无界队列
new ThreadPoolExecutor.AbortPolicy()
);
// 提交1000个任务(队列无限增长)
for (int i = 0; i < 1000; i++) {
int taskId = i;
pool.execute(() -> {
try {
System.out.println("任务" + taskId + "排队等待处理");
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
pool.shutdown();
}
}
- 关键现象:
- 仅核心线程pool-1-thread-1处理任务
- 任务全部进入队列,不会创建非核心线程(因队列未填满)
- 不会触发拒绝策略(队列永远有空间)
今日总结与扩展
- 核心对比表
实验项 |
有界队列(ArrayBlockingQueue) |
无界队列(LinkedBlockingQueue) |
队列满时 |
触发线程扩容(到 maxPoolSize) |
不扩容(队列无限) |
拒绝策略 |
可能触发(任务数 > maxPoolSize + 队列容量) |
永不触发(队列不会满) |
内存风险 |
低(容量可控) |
高(可能堆积导致 OOM) |
- 扩展思考(5 分钟)
问题 1:生产环境为什么不建议用无界队列?
答案:无界队列可能导致任务无限堆积,耗尽内存,典型案例:某电商活动因使用Executors.newFixedThreadPool(内部为无界队列)导致 OOM。
问题 2:如何计算有界队列的合理容量?
提示:根据任务处理耗时和预期并发量估算,例如:
核心线程数 = 10,每个任务平均耗时 100ms
队列容量 = 预期突发流量峰值 - 核心线程数 ×(超时时间 / 耗时)
工具与环境准备
- 代码要求:直接复制上述两个 Java 文件,分别运行观察结果
- JVM 参数:无需特殊配置,直接运行
- 调试技巧:
- 在pool.execute()前打印任务提交时间
- 使用pool.getQueue().size()实时查看队列长度
✅ 今日任务 checklist
- ✅ 理解有界队列与无界队列的核心区别
- ✅ 成功运行两个实验,观察到拒绝策略触发与不触发的差异
- ✅ 记录 1 个生产环境队列选择的思考(如:订单系统用有界队列,日志系统用无界队列)