先带大家回顾一下策略是如何触发执行的流程:
//添加任务,当不满足条件时会执行拒绝方法reject
public void execute(Runnable command) {
//...
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
//这里就是拒绝的入口。handler是有构造方法传入
final void reject(Runnable command) {
handler.rejectedExecution(command, this);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//....
//指定拒绝策略
this.handler = handler;
}
AbortPolicy:默认的拒绝策略,简单粗暴,当 execute 中添加 woker 失败时,直接在当前线程抛出异常。
public static class AbortPolicy implements RejectedExecutionHandler {
//直接抛出RejectedExecutionException
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
优点:快速失败,立即暴露系统过载问题、避免任务静默丢失,便于监控系统捕获
缺点:需要调用方显式处理异常,增加代码复杂度,可能中断主业务流程
适用场景:适用于那些对任务丢失非常敏感,配合熔断机制使用的快速失败场景
CallerRunsPolicy:提交任务的线程,直接执行任务
public static class CallerRunsPolicy implements RejectedExecutionHandler {
//直接在提交任务的线程中执行任务
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
优点:任务都会被执行,不会丢任务,并且由于主线程执行任务,天然的流量控制,避免了大量的任务进入线程池。
缺点:调用线程可能被阻塞,导致上游服务雪崩。不适合高并发场景(可能拖垮整个调用链)。
适用场景:适用于处理能力不高,并且资源过载能够平滑过渡,同时不丢失任务的场景。如:低并发、高可靠性的后台任务(如日志归档)、允许同步执行的批处理系统。
DiscardPolicy:直接丢弃被拒绝的任务,不做任何通知。
public static class DiscardPolicy implements RejectedExecutionHandler {
//空实现
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
优点:实现简单,无额外性能开销。避免异常传播影响主流程
缺点:数据静默丢失,可能会掩盖系统容量问题
适用场景:边缘业务的监控上报数据,统计类的 uv、pv 统计任务
DiscardOldestPolicy:丢弃队列中最旧的任务,然后重试提交当前任务。
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
//丢弃队列中最旧的任务,重试当前任务
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
优点:优先保证新任务执行,避免队列堆积导致内存溢出。
缺点:可能丢失关键旧任务、任务执行顺序无法保证。
适用场景:适用于可容忍部分数据丢失,并且实时性要求高于历史数据的场景,比如:行情推送。
通过上线的介绍,我们可以看到 JDK 内置策略基本上只使用于简单处理的场景,在生产实践中一般推荐我们自定义拒绝策略,进行相关的业务处理。
1. 自定义 RejectedExecutionHandler:
/**
* 带监控统计的拒绝策略处理器
*/
public class MetricsRejectedExecutionHandler implements RejectedExecutionHandler {
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(MetricsRejectedExecutionHandler.class);
// 统计被拒绝的任务数量
private final AtomicLong rejectedCount = new AtomicLong(0);
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 1. 采集线程池关键指标
int poolSize = executor.getPoolSize();
int activeThreads = executor.getActiveCount();
int corePoolSize = executor.getCorePoolSize();
int maxPoolSize = executor.getMaximumPoolSize();
int queueSize = executor.getQueue().size();
long completedTasks = executor.getCompletedTaskCount();
// 2. 递增拒绝计数器
long totalRejected = rejectedCount.incrementAndGet();
// 3. 输出警告日志(包含完整指标)
logger.warn("""
任务被拒绝执行!线程池状态:
|- 活跃线程数/当前线程数: {}/{}
|- 核心/最大线程数: {}/{}
|- 队列大小: {}
|- 已完成任务数: {}
|- 历史拒绝总数: {}
|- 被拒绝任务: {}
""",
activeThreads, poolSize,
corePoolSize, maxPoolSize,
queueSize,
completedTasks,
totalRejected,
r.getClass().getName());
// 4. 可选:降级处理(如存入数据库等待重试)
// fallbackToDatabase(r);
// 5. 抛出RejectedExecutionException(保持默认行为)
throw new RejectedExecutionException("Task " + r.toString() + " rejected");
}
// 获取累计拒绝次数(用于监控)
public long getRejectedCount() {
return rejectedCount.get();
}
}
- 记录日志并告警:所有的异常处理中,最常见简单的方式无外乎,先记录个日志,然后有告警系统的进行相关的某书、某信以及短信等的告警信息推送,方便开发人员以及运维人员的及时发现问题并介入处理。
- 兜底处理机制:一般常见的就是通过异步的方式提交到 MQ,然后统一进行兜底处理。
- 带超时和重试的拒绝:可以尝试等待一小段时间,或者重试几次提交,如果仍然失败,再执行最终的拒绝逻辑(如告警、持久化或抛异常)。
- 动态调整策略:根据系统的负载或任务类型,动态的执行
40

被折叠的 条评论
为什么被折叠?



