以下是 Day 3 详细学习内容(线程池拒绝策略实战:DiscardOldestPolicy与CallerRunsPolicy,30 分钟完整计划),包含策略原理、分步代码实战和场景解析:
???? 今日学习目标
- 掌握DiscardOldestPolicy(丢弃最老任务)与CallerRunsPolicy(调用者执行)的核心逻辑
- 理解两种策略的适用场景(如日志系统 vs 用户请求系统)
- 实战:通过代码对比两种策略的不同行为
⏰ 时间分配
时间段 |
任务 |
详细内容 |
0-10 分钟 |
理论:拒绝策略深度解析 |
1. DiscardOldestPolicy原理:为何丢弃队列头部任务?2. CallerRunsPolicy优势:减缓任务提交速度3. 生产场景选择建议 |
10-25 分钟 |
实战:双策略对比实验 |
1. 编写DiscardOldestPolicy示例2. 编写CallerRunsPolicy示例3. 观察任务执行顺序与线程归属 |
25-30 分钟 |
总结与扩展 |
1. 记录两种策略的核心区别2. 思考:如何避免DiscardOldestPolicy导致关键任务丢失?3. 扩展:如何监控拒绝策略的触发次数? |
???? 理论详解:两种核心拒绝策略
- DiscardOldestPolicy(丢弃最老任务)
- 核心逻辑:
当任务无法处理时,丢弃队列中等待时间最长的任务(队列头部任务),然后尝试将新任务加入队列。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
- 适用场景:
- 实时性要求高的场景(如用户最新操作),允许牺牲旧任务(如股票行情更新)
- 日志系统:优先处理最新日志,旧日志可能已过时
- CallerRunsPolicy(调用者执行)
- 核心逻辑:
当任务无法处理时,由提交任务的线程(通常是主线程)直接执行任务,而不是由线程池中的工作线程执行。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
- 优势:
- 减缓任务提交速度:主线程执行任务时,后续提交会被阻塞,避免线程池被压垮
- 保护线程池:防止短时间内大量任务涌入导致系统崩溃
???? 实战步骤:双策略对比实验
- 实验 1:DiscardOldestPolicy(丢弃最老任务)
import java.util.concurrent.*;
public class DiscardOldestDemo {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(
1,
2,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
for (int i = 0; i < 4; i++) {
int taskId = i;
pool.execute(() -> {
System.out.println("执行任务:" + taskId + ",线程:" + Thread.currentThread().getName());
});
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
pool.shutdown();
}
}
- 任务 0 由核心线程执行
- 任务 1、2 进入队列(队列满)
- 任务 3 提交时,队列满且线程数未达最大,触发策略:丢弃队列头部任务 0,任务 3 入队
执行任务:0,线程:pool-1-thread-1
执行任务:1,线程:pool-1-thread-1(核心线程处理完0后处理1)
执行任务:3,线程:pool-1-thread-1(任务2被丢弃,任务3入队)
- 实验 2:CallerRunsPolicy(调用者执行)
public class CallerRunsDemo {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(
1,
1,
0,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1),
new ThreadPoolExecutor.CallerRunsPolicy()
);
for (int i = 0; i < 3; i++) {
int taskId = i;
pool.execute(() -> {
try {
Thread.sleep(500);
System.out.println("任务" + taskId + "执行,线程:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
pool.shutdown();
}
}
- 关键现象:
- 任务 0 由核心线程执行
- 任务 1 进入队列
- 任务 2 提交时,队列满且无线程扩展空间,触发策略:由主线程(main线程)执行任务 2
- 预期输出:
任务0执行,线程:pool-1-thread-1
任务1执行,线程:pool-1-thread-1(500ms后)
任务2执行,线程:main(主线程直接执行)
???? 今日总结与扩展
- 核心策略对比表
策略 |
丢弃任务? |
执行线程 |
适用场景 |
风险点 |
DiscardOldest |
是(最老) |
线程池线程 |
优先处理新任务(如实时数据) |
可能丢失重要的早期任务 |
CallerRuns |
否 |
调用者线程 |
保护线程池(如用户请求入口) |
主线程被阻塞,影响后续提交 |
- 扩展思考(5 分钟)
- 问题 1:如何统计拒绝策略的触发次数?
答案:自定义拒绝策略,继承RejectedExecutionHandler,重写rejectedExecution方法并添加计数器:
class CustomHandler implements RejectedExecutionHandler {
private AtomicInteger rejectCount = new AtomicInteger(0);
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
rejectCount.incrementAndGet();
}
}
- 问题 2:生产环境如何选择拒绝策略?
提示:
核心业务(如订单支付):用AbortPolicy,通过 try-catch 捕获异常并记录
非核心业务(如日志、监控):用DiscardPolicy或DiscardOldestPolicy
入口层服务(如 API 网关):用CallerRunsPolicy,避免客户端请求被直接拒绝
???? 工具与环境准备
- 代码要求:直接复制两个 Java 文件,分别运行观察输出
- 调试技巧:
在pool.execute()后添加System.out.println(“任务” + taskId + “提交”);,观察提交顺序
使用pool.getRejectedExecutionHandler()验证当前策略类型
✅ 今日任务 checklist
✅ 理解两种拒绝策略的核心逻辑
✅ 成功运行两个实验,观察到任务丢弃与调用者执行的差异
✅ 记录 1 个生产场景应用案例(如:用户注册接口用CallerRunsPolicy防止突发流量压垮线程池)