文章目录
- 参考
- 自定义ThreadPoolExecutor
- ScheduledThreadPoolExecutor
- 修改任务执行和加入任务队列顺序
- 设置任务完成时的回调
- 继续执行因为线程池已满而失败的任务
自定义ThreadPoolExecutor
学习了ThreadPoolExecutor的源码之后,我就在想怎么DIY它,首先很自然的,就是通过自定义构造方法参数来实现自己想要的功能,其中除了corePoolSize
这些参数指标之外,操作空间最大的就是任务队列workQueue
,从源码可知,线程池使用了workQueue
的take
,poll
和offer
来获取和添加任务,我们可以自定义这三个方法来达到改变线程池运行逻辑的目的;其次,可以继承ThreadPoolExecutor,ThreadPoolExecutor本身就预留很多protected
的空方法,比如afterExecute
和terminated
等,这些方法都是在线程池的某些状态节点调用,实现这些方法就能监听这些节点;最后更粗暴的就是,直接将ThreadPoolExecutor的所有代码复制出来变成自己的项目中的一个新类,这样我们就获得了随意修改线程池的能力。ThreadPoolExecutor实在厉害,这个类我们复制出来是可以直接使用的,因为它用到的所有类都是我们可以有权限获取到的。
ScheduledThreadPoolExecutor
首先来看一下ScheduledThreadPoolExecutor
是怎么通过改变ThreadPoolExecutor
来实现延时和周期执行任务的功能的,因为我学习的目的是了解它的思路,所以我没有完完整整的阅读代码,如果是想完整学习这个类的同学可以学习java并发编程笔记–ScheduledThreadPoolExecutor实现这篇文章。
首先要延时执行任务,任务队列就必须要有根据任务执行时间给任务排序的能力,这里我想到了优先队列,然后我想到,Looper的MessageQueue天然就是这样的,但是MessageQueue是通过单链表实现的,而优先队列是通过堆排序实现的,显然优先队列在排序上的效率更高,ScheduledThreadPoolExecutor
就采用了优先队列的思路,通过构建一个最小堆(一种父节点值均不大于其左子节点和右子节点的值的二叉树,对这种数据结构不熟悉的同学可以看一下堆排序以及最大优先队列这篇文章)来实现。有了这个队列,实现延时执行任务就很简单了,我们从队列中每次读取位于堆顶的任务(根据最小堆的特性,这个任务一定是执行时间最前的任务),如果最前的任务仍然未到该执行的时间,就通过阻塞线程来达到目的;而周期执行任务,可以通过执行完任务后,更新该任务的执行时间,再次加入任务队列来实现。
修改任务执行和加入任务队列顺序
第一次知道线程池的时候,看线程池的参数,我的第一直觉线程池的运行规则应该是先运行核心线程,核心线程满了就运行非核心线程,非核心线程也满了之后就放入任务队列。而显然,我的直觉是错的。但是如果想让线程池以这种规则运行的话应该怎么办呢?
由代码可知,ThreadPoolExecutor
中决定任务是立即启动线程执行还是放入任务队列是在execute
中,我最初的想法就是修改execute
的逻辑,将启动非核心线程的判断放到加入任务队列的判断之前,就可以达到效果。后来我看到了大佬的这篇文章扩展ThreadPoolExecutor的一种办法,大佬提出了一个更优雅的方法:execute
通过任务队列的offer
方法来判断能否加入任务队列,我们可以通过自定义任务队列的方法修改offer
的实现,当线程数没有达到最大值的时候,一直返回fasle,从而也达到了执行非核心线程在放入任务队列之前的目的。
更好的想法是,使用这种方法,我们增加一个参数,当任务队列中的任务达到某个值时,我们就增加非核心线程来执行任务,而不必等到任务队列已满再增加,以避免设置任务队列设置长度就有任务已满被拒绝可能、不限长度就有任务等待时间过长的风险,或者任务队列长度为0,不限线程数量就可能线程太多这些问题。
线程池还有一个反直觉的地方就是因为先添加任务被加入任务队列后,后添加的任务反而更先执行的问题,要保证越早添加的任务越早执行,我们同样可以修改execute
方法,在任务队列已满,要启动非核心线程的时候,将任务队列最前的任务取出,再将最新添加任务放入任务队列。
设置任务完成时的回调
当我们想知道放入线程池的任务执行完成的消息时,我们可以通过在任务最后添加一个回调,或者实现ThreadPoolExecutor的afterExecute
方法,这个方法在任务执行完成后调用,并将这个任务作为参数。这两种方法都能达到目的,但是如果你想在任务执行完成的时候执行的操作跟线程池本身状态有关,严格来说,跟当前线程池已完成任务数量有关的话,就要小心了,以下是线程池执行所有任务的方法runWorker
中的一段代码:
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x;
throw x;
} catch (Error x) {
thrown = x;
throw x;
} catch (Throwable x) {
thrown = x;
throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;//完成任务数加一
w.unlock();
}
可以看到,完成任务数量completedTasks
(这个变量严格来说记录的是当前线程已完成任务数量,但是总完成任务数量也是每个线程完成的数量相加,所以我就统而言之了)增加是在run
和afterExecute
之后,所以当我们在这两个方法中读取已完成任务的话,是没有将当前这个任务统计进去的,这里就有了个概念比较含糊的地方。我想到的办法就是在finally
中w.completedTasks++
之后再添加一个回调方法来达到目的,但如果采用这种方式,这个方法中就不应该执行什么危险的操作,或者要处理好异常。
继续执行因为线程池已满而失败的任务
当任务池已满再添加任务时,就会执行拒绝策略。默认的拒绝策略是抛出异常或者忽略掉之类的,但是如果我们想要正常执行所有任务呢? 最简单的方法就是保证不触发拒绝策略:设定无限长的任务队列或者不限线程数量。但这样就会有我们前面提到的各种可能的问题(如果不另行DIY的话)。我想到的办法是在拒绝策略中增加第二个不限数量的任务队列,将触发拒绝策略的任务添加到第二个任务队列中,监听afterExecute
方法,等有任务执行完成后,再将这个队列中的任务添加到线程池中去,这个方法本质上跟修改任务队列offer
方法设置阈值是一样的,但是如果想监听任务队列已满的状态,或者对拒绝的任务做其他处理的话,这个方法要更简单一点。