当线程池忙不过来时,如何拒绝新任务?是直接抛异常、让调用者自己执行,还是丢弃任务?选择不同,结果大不同!
一、开场:线程池什么时候会拒绝任务?🤔
触发拒绝的条件
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲存活时间
new ArrayBlockingQueue<>(10), // 队列容量10
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
拒绝时机:
1. 线程数 < 核心线程数 → 创建新线程 ✅
2. 线程数 = 核心线程数 → 放入队列 ✅
3. 队列已满 + 线程数 < 最大线程数 → 创建新线程 ✅
4. 队列已满 + 线程数 = 最大线程数 → 🚫 拒绝!
可视化:
任务到来
↓
核心线程忙吗?
├─ 否 → 创建核心线程执行
└─ 是
↓
队列满吗?
├─ 否 → 放入队列
└─ 是
↓
达到最大线程数?
├─ 否 → 创建临时线程
└─ 是 → 💥 执行拒绝策略!
二、JDK自带的4种拒绝策略🎯
策略1:AbortPolicy(中止策略)⛔ 默认
public static class AbortPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException(
"Task " + r.toString() + " rejected from " + e.toString()
);
}
}
特点:
- 直接抛出
RejectedExecutionException - 不执行任务
- 调用者可以捕获异常处理
适用场景:
- 任务绝对不能丢失
- 需要调用者感知拒绝并处理
示例:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1),
new ThreadPoolExecutor.AbortPolicy() // 默认策略
);
try {
executor.execute(() -> System.out.println("任务1"));
executor.execute(() -> System.out.println("任务2"));
executor.execute(() -> System.out.println("任务3")); // 💥 抛异常
} catch (RejectedExecutionException e) {
System.out.println("任务被拒绝: " + e.getMessage());
// 处理:记录日志、重试、降级
}
输出:
任务1
任务被拒绝: Task ... rejected from ...
策略2:CallerRunsPolicy(调用者运行策略)🏃
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run(); // 在调用者线程执行!
}
}
}
特点:
- 不抛异常
- 在调用者线程执行任务
- 会阻塞调用者线程
适用场景:
- 任务不能丢失
- 可以容忍调用者线程被阻塞
- 自然的背压机制(反压)
示例:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1),
new ThreadPoolExecutor.CallerRunsPolicy()
);
System.out.println("主线程: " + Thread.currentThread().getName());
executor.execute(() -> {
System.out.println("任务1: " + Thread.currentThread().getName());
sleep(1000);
});
executor.execute(() -> {
System.out.println("任务2: " + Thread.currentThread().getName());
});
executor.execute(() -> {
System.out.println("任务3: " + Thread.currentThread().getName()); // 主线程执行!
});
输出:
主线程: main
任务1: pool-1-thread-1
任务3: main ← 在主线程执行
任务2: pool-1-thread-1
优点:
- 自动降速(调用者被阻塞,提交速度降低)
- 任务不丢失
缺点:
- 调用者线程被占用
- 可能影响调用者的其他任务
策略3:DiscardPolicy(丢弃策略)🗑️
public static class DiscardPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// 什么都不做,静默丢弃
}
}
特点:
- 不抛异常
- 直接丢弃任务
- 调用者不知道任务被拒绝
适用场景:
- 任务可以丢失
- 不重要的日志、统计
- 对实时性要求高,旧数据可以丢弃
示例:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1),
new ThreadPoolExecutor.DiscardPolicy()
);
executor.execute(() -> System.out.println("任务1"));
executor.execute(() -> System.out.println("任务2"));
executor.execute(() -> System.out.println("任务3")); // 静默丢弃
Thread.sleep(2000);
executor.shutdown();
输出:
任务1
任务2
(任务3被丢弃,没有输出)
⚠️ 危险: 任务丢失,调用者不知道,可能导致数据丢失!
策略4:DiscardOldestPolicy(丢弃最老任务策略)🕰️
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll(); // 丢弃队列头部任务
e.execute(r); // 重新提交当前任务
}
}
}
特点:
- 丢弃队列中最老的任务
- 然后重新提交当前任务
- 不抛异常
适用场景:
- 新任务优先级更高
- 旧数据可以丢弃(如实时监控)
示例:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(2),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
executor.execute(() -> {
System.out.println("任务1");
sleep(1000);
});
executor.execute(() -> System.out.println("任务2")); // 进队列
executor.execute(() -> System.out.println("任务3")); // 进队列
executor.execute(() -> System.out.println("任务4")); // 丢弃任务2,任务4进队列
Thread.sleep(2000);
executor.shutdown();
输出:
任务1
任务3
任务4
(任务2被丢弃)
三、四种策略对比表📊
| 策略 | 任务丢失 | 抛异常 | 阻塞调用者 | 适用场景 |
|---|---|---|---|---|
| AbortPolicy | ❌ 不丢失 | ✅ 抛异常 | ❌ 不阻塞 | 不能丢任务,需感知拒绝 |
| CallerRunsPolicy | ❌ 不丢失 | ❌ 不抛 | ✅ 阻塞 | 背压机制,自动降速 |
| DiscardPolicy | ✅ 丢失 | ❌ 不抛 | ❌ 不阻塞 | 任务可丢失,如日志 |
| DiscardOldestPolicy | ✅ 丢失旧的 | ❌ 不抛 | ❌ 不阻塞 | 新任务优先级高 |
四、自定义拒绝策略💡
场景1:记录日志并告警
public class LogAndAlertPolicy implements RejectedExecutionHandler {
private static final Logger log = LoggerFactory.getLogger(LogAndAlertPolicy.class);
private final AtomicLong rejectedCount = new AtomicLong(0);
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
long count = rejectedCount.incrementAndGet();
// 记录日志
log.error("任务被拒绝! 任务: {}, 线程池状态: 活跃线程={}, 队列大小={}, 总拒绝数={}",
r.toString(),
executor.getActiveCount(),
executor.getQueue().size(),
count
);
// 告警(拒绝次数超过阈值)
if (count % 100 == 0) {
sendAlert("线程池拒绝任务数达到: " + count);
}
// 抛异常通知调用者
throw new RejectedExecutionException("任务被拒绝,已记录日志");
}
private void sendAlert(String message) {
// 发送告警:短信、邮件、钉钉等
System.out.println("🚨 告警: " + message);
}
}
场景2:异步降级存储
public class AsyncFallbackPolicy implements RejectedExecutionHandler {
private final BlockingQueue<Runnable> fallbackQueue;
private final ScheduledExecutorService retryExecutor;
public AsyncFallbackPolicy() {
this.fallbackQueue = new LinkedBlockingQueue<>(10000);
this.retryExecutor = Executors.newSingleThreadScheduledExecutor();
// 定时重试
retryExecutor.scheduleWithFixedDelay(this::retryTasks, 1, 1, TimeUnit.SECONDS);
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 放入降级队列
boolean added = fallbackQueue.offer(r);
if (!added) {
// 降级队列也满了,记录并丢弃
log.error("降级队列已满,任务丢弃: {}", r);
} else {
log.warn("任务降级到备用队列: {}", r);
}
}
private void retryTasks() {
// 从降级队列取任务重试
Runnable task;
while ((task = fallbackQueue.poll()) != null) {
try {
task.run(); // 直接执行或提交到其他线程池
} catch (Exception e) {
log.error("降级任务执行失败", e);
}
}
}
}
场景3:持久化到数据库
public class PersistencePolicy implements RejectedExecutionHandler {
private final TaskRepository taskRepository;
public PersistencePolicy(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (r instanceof PersistableTask) {
PersistableTask task = (PersistableTask) r;
// 保存到数据库
taskRepository.save(task);
log.warn("任务被拒绝,已保存到数据库,taskId: {}", task.getTaskId());
} else {
// 无法持久化,抛异常
throw new RejectedExecutionException("任务被拒绝且无法持久化");
}
}
}
// 可持久化的任务
interface PersistableTask extends Runnable {
String getTaskId();
String serialize();
}
场景4:动态路由到备用线程池
public class FailoverPolicy implements RejectedExecutionHandler {
private final List<ThreadPoolExecutor> backupPools;
private final AtomicInteger currentIndex = new AtomicInteger(0);
public FailoverPolicy(List<ThreadPoolExecutor> backupPools) {
this.backupPools = backupPools;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 轮询备用线程池
for (int i = 0; i < backupPools.size(); i++) {
int index = currentIndex.getAndIncrement() % backupPools.size();
ThreadPoolExecutor backup = backupPools.get(index);
try {
backup.execute(r);
log.info("任务转移到备用线程池-{}", index);
return;
} catch (RejectedExecutionException e) {
// 备用池也满了,尝试下一个
continue;
}
}
// 所有备用池都满了
log.error("所有线程池都已满,任务被丢弃");
throw new RejectedExecutionException("所有线程池都已满");
}
}
五、生产环境如何选择?🏭
决策树
任务能丢失吗?
├─ 不能丢失
│ ├─ 需要感知拒绝?
│ │ ├─ 是 → AbortPolicy + 异常处理 ⭐
│ │ └─ 否 → CallerRunsPolicy(背压) ⭐
│ └─ 有备用方案?
│ └─ 是 → 自定义(降级/持久化/备用池)⭐
└─ 可以丢失
├─ 新任务更重要?
│ └─ 是 → DiscardOldestPolicy
└─ 直接丢弃 → DiscardPolicy
不同场景的推荐策略
1. Web请求处理
// 推荐:CallerRunsPolicy(背压)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, 50, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy()
);
原因:
- 请求不能丢失
- 调用者线程执行,自然降速
- 防止线程池压力过大
2. 异步日志
// 推荐:DiscardPolicy(丢弃)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10000),
new ThreadPoolExecutor.DiscardPolicy()
);
原因:
- 日志丢失影响不大
- 不能影响主业务
- 简单高效
3. 订单处理
// 推荐:自定义(告警+持久化)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
20, 100, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(5000),
new LogAndAlertPolicy() // 自定义
);
原因:
- 订单绝对不能丢
- 需要告警通知
- 需要监控拒绝次数
4. 实时监控数据
// 推荐:DiscardOldestPolicy(丢弃旧数据)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
原因:
- 监控数据只关心最新的
- 旧数据可以丢弃
- 实时性要求高
六、生产环境最佳实践✅
1. 合理配置线程池参数
// ❌ 错误:使用Executors创建
ExecutorService executor = Executors.newFixedThreadPool(10);
// ✅ 正确:手动配置
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, // 核心线程数
50, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲存活时间
new LinkedBlockingQueue<>(1000), // 有界队列
new ThreadFactoryBuilder()
.setNameFormat("业务线程池-%d")
.build(),
new CustomRejectedPolicy() // 自定义策略
);
2. 监控线程池状态
public class ThreadPoolMonitor {
private final ThreadPoolExecutor executor;
private final AtomicLong totalRejected = new AtomicLong(0);
public void monitor() {
ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor();
monitor.scheduleAtFixedRate(() -> {
int activeCount = executor.getActiveCount();
int poolSize = executor.getPoolSize();
int queueSize = executor.getQueue().size();
long completedTasks = executor.getCompletedTaskCount();
log.info("线程池状态: 活跃线程={}, 线程数={}, 队列={}, 已完成={}, 总拒绝={}",
activeCount, poolSize, queueSize, completedTasks, totalRejected.get()
);
// 告警
if (queueSize > 800) {
alert("队列堆积严重: " + queueSize);
}
if (activeCount == poolSize && poolSize == executor.getMaximumPoolSize()) {
alert("线程池已满负荷运行");
}
}, 10, 10, TimeUnit.SECONDS);
}
}
3. 优雅关闭
public void shutdown() {
log.info("开始关闭线程池...");
// 1. 停止接受新任务
executor.shutdown();
try {
// 2. 等待60秒
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
// 3. 强制关闭
log.warn("等待超时,强制关闭");
List<Runnable> droppedTasks = executor.shutdownNow();
log.warn("丢弃任务数: {}", droppedTasks.size());
// 4. 再等30秒
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
log.error("线程池无法关闭");
}
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
log.info("线程池已关闭");
}
4. 根据场景动态调整
public class DynamicThreadPool {
private final ThreadPoolExecutor executor;
// 根据负载动态调整
public void adjustPoolSize() {
int queueSize = executor.getQueue().size();
int corePoolSize = executor.getCorePoolSize();
if (queueSize > 500 && corePoolSize < 50) {
// 队列堆积,增加核心线程
executor.setCorePoolSize(corePoolSize + 5);
log.info("增加核心线程数至: {}", corePoolSize + 5);
}
if (queueSize < 100 && corePoolSize > 10) {
// 队列空闲,减少核心线程
executor.setCorePoolSize(corePoolSize - 5);
log.info("减少核心线程数至: {}", corePoolSize - 5);
}
}
}
七、面试高频问答💯
Q1: 四种拒绝策略的区别?
A:
- AbortPolicy:抛异常,不丢任务,调用者感知
- CallerRunsPolicy:调用者线程执行,背压机制
- DiscardPolicy:静默丢弃,危险
- DiscardOldestPolicy:丢弃最老任务,新任务优先
Q2: 生产环境推荐哪种策略?
A:
- 默认推荐:CallerRunsPolicy(背压)
- 重要任务:AbortPolicy + 异常处理
- 日志等不重要任务:DiscardPolicy
Q3: 如何监控线程池拒绝情况?
A: 自定义拒绝策略,在rejectedExecution中:
- 计数器统计拒绝次数
- 记录日志
- 发送告警
Q4: CallerRunsPolicy会影响性能吗?
A: 会,但这是好事(背压):
- 调用者线程被占用,提交速度降低
- 自然保护线程池,防止过载
- 任务不丢失
Q5: 为什么不推荐DiscardPolicy?
A: 因为静默丢弃很危险:
- 调用者不知道任务被丢弃
- 可能导致数据丢失
- 难以排查问题
八、总结:拒绝策略选型指南🎯
核心原则
- 不丢任务:AbortPolicy 或 CallerRunsPolicy
- 需要感知:AbortPolicy
- 自动背压:CallerRunsPolicy
- 可以丢失:DiscardPolicy(谨慎)
- 新数据优先:DiscardOldestPolicy
- 生产环境:自定义(告警+监控)
配置模板
// 通用模板
ThreadPoolExecutor executor = new ThreadPoolExecutor(
核心线程数,
最大线程数,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(队列大小),
new ThreadFactoryBuilder()
.setNameFormat("业务-%d")
.setUncaughtExceptionHandler((t, e) -> log.error("线程异常", e))
.build(),
new CallerRunsPolicy() // 或自定义策略
);
// 添加监控
monitorThreadPool(executor);
下期预告: 如何实现一个支持超时的线程安全缓存?LRU+过期策略!🗄️