一. 前言
1.1 什么是拒绝策略
线程池是业务中常用的线程调度工具,说到线程池原理,大家应该都了解过:corePoolSize,maximumPoolSize,workQueue等等。
当workThread没有空闲且workQueue已经填满时,说明线程池当前没有消费任务的能力了,那么再多出来的任务应该如何处理,这就是拒绝策略。
1.2 拒绝策略的重要性
拒绝策略被Executer包装的比较好,很多开发者都没有显示配置过拒绝策略,在并发比较低或者并发量可预期的场景下没有什么问题。
实际的业务中,在并发量超出预期时,因为拒绝策略选择不当引发的问题还不少, 比如:
- 一批任务中某些没有执行
- 线程池执行异常退出
- 应用CPU突然升高
这些问题会导致系统逻辑不可控,且大多表征不明,需要调查好久才能解决问题。
1.3 JUC内置的策略
在ThreadPoolExecutor中内置了4个拒绝策略
- AbortPolicy
- DiscardPolicy
- CallerPolicy
- DiscardOldestPolicy
并且维护 defaultHandler = new AbortPolicy(); 因此当我们没有显示指定拒绝策略时,默认使用的是AbortPolicy策略。
定位到源码可以看到:这些策略都是对于RejectedExecutionHandler的实现,核心逻辑在rejectedExecution方法中。
public static class AbortPolicy implements RejectedExecutionHandler {
/**
* Creates an {@code AbortPolicy}.
*/
public AbortPolicy() { }
// r:任务,e:线程池
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
下文我们通过用例测试一下这4个策略效果和实现的原理。
二. 内置策略的测评
2.1 测评用例
- 线程池构建
public static ThreadPoolExecutor buildPolicyPool(RejectedExecutionHandler policy) {
return new ThreadPoolExecutor(1,
1, 10, TimeUnit.MINUTES, new ArrayBlockingQueue<>(5),
policy);
}
构造方法,指定核心线程数和最大线程数都为1,等待队列长度为5,参数为不同的执行策略。
- 执行方法
方法内部创建10个线程送往待测线程池中执行。
public static void exec(ThreadPoolExecutor pool) {
AtomicInteger taskNum = new AtomicInteger();
for (int i = 0; i < 10; i++) {
String taskName = "任务" + i;
// 为了保证异常不影响整个线程池运行,加个异常捕获。
try {
pool.submit(() -> {
try {
// 因为线程池只有一个线程,所以workThread一直是一个
System.out.println("threadName:" + Thread.currentThread().getName());
// 模拟任务执行时间长,队列满的情况
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
} catch (Exception e) {
System.out.println(taskName + "被拒绝");
}
}
// 保证主线程不停止,增加延迟时间
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
2.2 AbortPolicy
- 源码实现
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
仅抛出了异常,没有其他处理,为了正常运行,这里把异常catch住了。
- 评测结果
/**
* description:拒绝并抛出异常, Executor工厂模式生成pool的默认策略
* expect:前6个任务正常执行,后4个任务全部拒绝抛出异常
* analysis: 一共10个任务,coreSize = maxSize = 1,第1个任务被分配执行,然后5个依次进入等待队列,最后剩余4个被丢弃
*/
exec(buildPolicyPool(new ThreadPoolExecutor.AbortPolicy()));
threadName:pool-1-thread-1
任务6被拒绝
任务7被拒绝
任务8被拒绝
任务9被拒绝
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
2.3 DiscardPolicy
- 源码实现
public static class DiscardPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardPolicy}.
*/
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
直接丢弃,什么都没有做....
- 评测结果
* description:默默拒绝,不抛异常
* expect:前6个任务正常执行,后四个任务全部拒绝但不抛出异常
* analysis:逻辑同上,只是不抛异常了
*/
exec(buildPolicyPool(new ThreadPoolExecutor.DiscardPolicy()));
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
2.4 CallerPolicy
- 源码实现
public static class CallerRunsPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code CallerRunsPolicy}.
*/
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
如果线程池没有down,那么就由添加到pool的线程(主线程)去执行这个任务。
- 评测结果
/**
* description:由调用线程池的主线程执行任务,直到线程池结束
* expect:前6个任务由线程池线程执行,后4个任务由主线程执行2个,线程池线程执行2个
* analysis:前6个逻辑同上,后面任务因为每个任务时间恒定,因此剩余任务相当于主线程和工作线程同步执行,没个线程执行2个任务。
*/
exec(buildPolicyPool(new ThreadPoolExecutor.CallerRunsPolicy()));
threadName:pool-1-thread-1
threadName:main
threadName:pool-1-thread-1
threadName:main
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
2.5 DiscardOldestPolicy
- 源码实现
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardOldestPolicy} for the given executor.
*/
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
如果线程池没有down,从等待队列头部移除一个任务(新加的任务顺势到队尾)然后继续运行。
- 评测结果
/**
* description:从队列头部移除任务,让新任务能加进来继续执行,直到线程池结束
* expect:只执行6个任务
* analysis:前6个逻辑同上,后面的任务依次替换前面的任务,最终只执行 workThreadSize + queueSize = 6个
*/
exec(buildPolicyPool(new ThreadPoolExecutor.DiscardOldestPolicy()));
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
三. 自定义策略实现
从上文我们可以看到JUC内置的策略不过就是实现了RejectedExecutionHandler,那我们也可以自己实现一个和业务最契合的策略。
public static LinkedBlockingQueue<Runnable> businessQueue = new LinkedBlockingQueue();
public static class MyselfPolicy implements RejectedExecutionHandler {
// 这里简单用一个队列模拟补偿,也可以入库,发送mq等方式实现。
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
businessQueue.add(r);
System.out.println("进入业务补偿处理队列中:" + r.toString());
}
}
执行一下测试用例
/**
* description:自定义队列,进入等待队列中
* expect:成功执行前6个任务,后4个任务进入补偿队列中
* analysis:前6个逻辑同上,后面的任务走自定义逻辑
*/
exec(buildPolicyPool(new MyselfPolicy()));
threadName:pool-1-thread-1
进入业务补偿处理队列中:java.util.concurrent.FutureTask@12781b30
进入业务补偿处理队列中:java.util.concurrent.FutureTask@16fbbf52
进入业务补偿处理队列中:java.util.concurrent.FutureTask@29674c82
进入业务补偿处理队列中:java.util.concurrent.FutureTask@9c20ebf
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
四. 总结
每种拒绝策略的选型参考如下:
-
AbortPolicy
直接丢弃抛出异常,需要调用时catch打印一个日志告警,适用于对任务结果不敏感,可以重试场景。
-
DiscardPolicy
没有任何反应,只是丢弃了,丢失任务业务完全无感知,不建议使用。
-
CallerPolicy
业务线程会介入任务处理,对于并发比较高的场景慎用,会出现业务线程被占用导致整个服务瘫痪。
-
DiscardOldestPolicy
丢弃老任务,保证新任务执行,看似合理实际简单粗暴,除非业务和它的思想完全一致,否则不建议使用,出现问题很难回溯。
默认的策略无论哪种实现都比较简陋,如果要做到契合业务,还是自定义一个策略来实现更好。