简介
想必各位Java开发者对于线程池应该都不陌生吧,不知道大家有没有在创建线程池时,有没有关注到一个容易被忽略但又很重要的参数,那就是线程池的拒绝策略。这篇文章我主要跟各位开发者朋友介绍一下线程池拒绝策略在何时发生,以及发生了该如何处理。
何时会触发线程池拒绝策略
线程池的拒绝策略是在任务不能再提交的时候被触发的。主要分为以下两种情况:
- 线程池调用了
shutdown
等方法关闭线程池之后,线程池将拒绝继续提交任务,如果继续提交任务就会触发拒绝策略。 - 线程池任务数占满,队列占满,导致无法继续提交任务,触发拒绝策略。
线程池任务数占满,队列占满的情况,通过线程池的几个核心参数进行分析:
corePoolSize
:表示核心线程数,即线程池工作时能够同时并行的核心线程数量。workQueue
:表示缓冲队列,主要在线程池核心线程数占满的情况,对后续提交的任务进行缓冲,等待有核心线程任务执行完后从队列中获取任务继续执行。maximumPoolSize
:最大线程数,在核心线程数任务占满且缓存队列占满的情况下,线程池会额外创建新线程,用于辅助任务执行。
日常开发中容易忽略拒绝策略的情况:
- 并发量达不到,无法触发拒绝策略
- 默认拒绝策略直接抛出异常,通常没有对线程的运行进行异常捕获,即有可能出现异常未发现。
- 无关紧要的任务放入线程池执行,即使出异常也可忽略不管。
拒绝策略分析
所有拒绝策略都必须实现RejectedExecutionHandler
接口,这里我们就来聊一聊ThreadPoolExecutor
当中内置的几种拒绝策略,以及我们该如何自定义拒绝策略。
RejectedExecutionHandler
接口主要就是一个执行拒绝策略的抽象方法rejectedExecution
,即提交任务被拒绝时要执行的动作。
内置拒绝策略
在JUC
当中的ThreadPoolExecutor
提供了几种线程池拒绝策略:
以下示例我们为了方便触发拒绝策略,统一进行参数配置:核心线程数、最大线程数等于1,队列大小等于1,线程池执行的任务休眠3秒,这样一来我们只需使用这个线程池运行3个任务,都会触发拒绝策略。
CallerRunsPolicy
直接在主线程执行任务:如图在rejectedExecution
方法中直接使用了r.run()
进行调用,直接调用Runnable
接口的run()
方法相当于在主线程直接运行,而不是在线程池中运行。
注:使用这种拒绝策略我们就该考虑是否影响主线程的运行,特别在任务量较大的情况,直接使用主线程运行,势必会影响到其他业务的执行。切记使用此拒绝策略的线程池应当满足几个要求:
- 并发量不大,确保触发拒绝策略的概率较小。
- 避开系统业务高峰期,适用可以在半夜系统使用较少的时间段执行的线程池。
上图我们根据运行日志就可以看到,0、1两个任务都是在线程池中执行,而任务2则由于线程池资源占满拒绝提交任务,从而触发拒绝策略直接在主线程中执行任务。
AbortPolicy
直接抛出异常:下图我们可以看到,在rejectedExecution
直接抛出一个异常。
注:该拒绝策略直接抛出异常,将会直接导致我们运行线程池的主线程直接中断,如果我们在线程池执行完成后有后续的流程要处理,那么将收到影响,所以使用这种拒绝策略时就应该考虑异常发生的情况要如何对后续的流程做补偿。
上图可见当触发拒绝策略出现异常时,主线程中进行异常捕获处理,但是线程池任务执行完后面的日志输出task execute finish!
则没有执行。
DiscardPolicy
丢弃任务,什么都不处理:如下图可见rejectedExecution
中什么代码都没有,就是什么都不做,此方式针对不重要的任务即可直接不理会,否则出现问题时将很难排查到。
如图即便任务数再多,当线程池占满时,这些多余的任务都是直接被丢弃不做任何处理,也没有日志输出,若是一些重要的流程,我们将无法排查到问题所在。
DiscardOldestPolicy
从缓存队列获取下一个即将执行的任务,并进行忽略,重新提交新进来的任务。如下图可见rejectedExecution
中使用e.getQueue().poll()
获取队列中下一个将要执行的任务并忽略掉,接着使用e.execute(r)
重新提交当前任务。
注:被忽略掉的任务若有问题也将无法进行排查,此方法使用也需谨慎。
从上图运行情况来看,当任务0提交到线程池执行时,再提交任务1的时候将被加入队列中,任务2提交时将忽略掉队列中的任务1,以此类推最后能够成功执行的只有最后一个提交的任务,因为最后提交的就会存在队列中进行等待。
以上四种策略中AbortPolicy
作为ThreadPoolExecutor
当中的默认拒绝策略:
自定义拒绝策略
当我们无法预料什么时候线程池会触发拒绝策略,以及不知道触发拒绝策略时该怎么对任务进行重新执行或者进行其他处理时,这时候我们就很有必要自定义拒绝策略来解决这些问题。
需要友好的对被拒绝的任务进行优化处理时,我们就必须知道当前被拒绝的是哪个任务,那么这种情况我们就需要自定义实现拒绝策略和实现Runnable
接口。
java
@Slf4j
public class ThreadDemo {
public static void main(String[] args) {
BlockingDeque blockingDeque = new LinkedBlockingDeque(1);
RejectedExecutionHandler handler = new MyRejectedExecutionHandler();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 30, TimeUnit.SECONDS, blockingDeque, handler);
try {
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
threadPoolExecutor.execute(new MyRunnable(i));
}
else {
threadPoolExecutor.execute(new YourRunnable(i));
}
}
log.info("task execute finish!");
}
catch (Exception e) {
log.error("task execute error");
}
finally {
threadPoolExecutor.shutdown();
}
}
static class MyRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (r instanceof MyRunnable) {
MyRunnable myRunnable = (MyRunnable) r;
log.info("MyRunnable task:{} handler", myRunnable.getId());
}
else if (r instanceof YourRunnable) {
YourRunnable yourRunnable = (YourRunnable) r;
log.info("YourRunnable task:{} handler", yourRunnable.getId());
}
else {
log.info("任务丢弃");
}
}
}
static class MyRunnable implements Runnable {
private int id;
MyRunnable(int id) {
this.id = id;
}
@Override
public void run() {
log.info("execute MyRunnable task:{}", id);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
static class YourRunnable implements Runnable {
private int id;
YourRunnable(int id) {
this.id = id;
}
@Override
public void run() {
log.info("execute YourRunnable task:{}", id);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
}
自定义拒绝策略分析:
如上代码我们通过创建两个自定义线程类
YourRunnable、MyRunnable
都实现Runnable
接口,分别用来执行各自的业务流程,这里为了方便测试直接进行休眠3秒。
MyRejectedExecutionHandler
类实现了RejectedExecutionHandler
拒绝策略接口,主要实现自定义拒绝策略,在rejectedExecution
方法中我们可以根据Runnable
参数进行类型转换,判断具体是那种类型的线程任务被拒绝了,并且针对不同类型被拒绝的任务进行不一样的处理。
上图为示例代码的运行结果,从图中可以看出仅有task0、task1分别在MyRunnable、YourRunnable
中执行成功,其他的全都触发拒绝策略,分别根据对应的Runnable
进行处理。
总结
自定义拒绝策略最好能够搭配自定义的线程类使用,这样就可以直接知道是哪种类型的任务执行失败,同时自定义的线程类我们可以携带必要的参数,在触发拒绝策略时即可根据这些参数进行后续流程处理。