线程池的拒绝策略
一.线程池拒绝策略简介
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
- AbortPolicy:丢弃任务并抛出RejectedExecutionException异常
- DiscardPolicy:丢弃任务,但是不抛出异常。
- CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
- DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
二.线程池拒绝策略复现思路
2.1.继承前篇
接下来我会通过代码来分别复现这四种拒绝策略,我们之前在一篇名为SpringBoot配置线程池的文章里面配置过线程池,所以我们直接用这个了。
因为我们在之前一篇文章SpringBoot配置线程池中配置的核心线程为CPU核数加一,最大线程数为CPU核数乘以二,而我的这台电脑的CPU核数为8,所以核心线程数为9,最大线程数为16,通过下面的配置文件可知队列数下面为1,最终得出该线程池可以同时容纳17个任务,当第十八个任务进来的时候则执行拒绝策略。
universe.thread.pool.executor.threadNamePrefix=threadPoolTaskExecutor-
universe.thread.pool.executor.queueCapacity=1
universe.thread.pool.executor.rejectedExecutionHandler=实现了RejectedExecutionHandler接口的全类名
universe.thread.pool.executor.keepAliveSeconds=60
2.2 代码实现逻辑
线程池复现的思路是我们可以先把前十七个任务放进入线程池后,让主线程睡眠一段时间后,查看线程的阻塞队列,然后再将第十八个任务加入线程后再查看其阻塞队列,具体代码实现逻辑如下所示。
/**
* 当标注的属性是接口时,其实注入的是这个接口的实现类, 如果这个接口有多个实现类,只使用@Autowired就会报错,
* 因为它默认是根据类型找,然后就会找到多个实现类bean,所有就不知道要注入哪个。然后它就会根据属性名去找。所以
* 如果有多个实现类可以配合@Qualifier(value=“类名”)来使用
*/
@Qualifier("threadPoolTaskExecutor")
@Autowired
private Executor executor;
@Test
void rejectedExecutionHandlerTest() throws InterruptedException {
//获得ThreadPoolTaskExecutor的对象threadPoolTaskExecutor
ThreadPoolTaskExecutor threadPoolTaskExecutor = (ThreadPoolTaskExecutor) executor;
//获取ThreadPoolExecutor的对象threadPoolExecutor
ThreadPoolExecutor threadPoolExecutor = threadPoolTaskExecutor.getThreadPoolExecutor();
//获取threadPoolExecutor的阻塞队列queue
BlockingQueue<Runnable> queue = threadPoolExecutor.getQueue();
//先将17个任务放入线程池(每1毫秒一次,每个任务分别执行1000毫秒)
for (int i = 0; i < 17; i++) {
Thread.sleep(1);
threadPoolTaskExecutor.execute(() -> {
try {
log.info("线程{}开始执行", Thread.currentThread().getName());
Thread.sleep(1000);
log.info("线程{}执行完毕", Thread.currentThread().getName());
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
//睡眠100毫秒,等前十七个任务全部放进线程池
Thread.sleep(100);
//查看前十七个任务放入后的阻塞队列
Runnable take = queue.element();
log.info("前十七个任务已加入线程池,阻塞队列中的任务为{}", take);
//将第18个任务任务放入线程池(该任务执行1毫秒)
threadPoolTaskExecutor.execute(() -> {
try {
log.info("线程{}开始执行,第十八个任务加了进来", Thread.currentThread().getName());
Thread.sleep(1);
log.info("线程{}执行完毕,第十八个任务加了进来", Thread.currentThread().getName());
} catch (Exception e) {
throw new RuntimeException(e);
}
});
//查看第18个任务加入之后的阻塞队列
take = queue.element();
log.info("加入第十八个任务后,阻塞队列中的任务为{}", take);
}
三.线程池拒绝策略复现实践
3.1.AbortPolicy
丢弃任务并抛出RejectedExecutionException异常,当第18个任务加进来的时候,线程池如果抛出RejectedExecutionException异常则复现成功。
比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
3.1.1.配置
我们通过配置文件将线程池的拒绝策略修改为AbortPolicy如下所示。
universe.thread.pool.executor.rejectedExecutionHandler=java.util.concurrent.ThreadPoolExecutor$AbortPolicy
3.1.2.复现
我们可以看到该线程执行到第18个任务的时候抛出了RejectedExecutionException异常。
3.2.DiscardPolicy
丢弃任务但是不抛异常,当第18个任务加进来的时候,线程池如果不抛出异常并且不执行第18个任务则复现成功。
使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。
3.2.1.配置
我们通过配置文件将线程池的拒绝策略修改为DiscardPolicy如下所示。
universe.thread.pool.executor.rejectedExecutionHandler=java.util.concurrent.ThreadPoolExecutor$DiscardPolicy
3.2.2.复现
我们可以看到该线程执行到第17个任务后就不再执行第18个任务了并且没有抛出异常。
3.3.CallerRunsPolicy
由调用线程处理该任务,当第18个任务加进来的时候,线程池如果将该任务交给main处理则证明复现成功。
3.3.1.配置
我们通过配置文件将线程池的拒绝策略修改为CallerRunsPolicy如下所示。
一般在不允许失败的、对性能要求不高、并发量较小的场景下使用。
universe.thread.pool.executor.rejectedExecutionHandler=java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy
3.3.2.复现
我们可以看到当第18个任务加进来的时候,线程池将该任务交给main处理。
3.4.DiscardOldestPolicy
丢弃队列最前面的任务,然后重新提交被拒绝的任务,当第18个任务加进来的时候,线程池如果将之前的任务从队列中踢出,并将该任务加入队列则复现成功。
这个策略还是会丢弃任务,丢弃时也是毫无声息,但是特点是丢弃的是老的未执行的任务,而且是待执行优先级较高的任务,此场景使用于发布消息和修改消息。
3.4.1.配置
我们通过配置文件将线程池的拒绝策略修改为DiscardOldestPolicy如下所示。
universe.thread.pool.executor.rejectedExecutionHandler=java.util.concurrent.ThreadPoolExecutor$DiscardOldestPolicy
3.4.2.复现
我们可以看到当第18个任务加进来的时候,线程池将之前的任务从队列中踢出,并将该任务加入队列(图中展示为队列中的任务前后地址不一样并且第十八个任务照常执行)。