目录
5.2、ScheduleThreadPoolExecutor
前言
有关Executor框架下的四大线程池源码理解,主要是创建工厂,和工作线程插入时候,源码运作。这里大致给出几种线程池的大致描述,援引自《java concurrency in pratice》
1、newFixedThreadPool:混合线程池,创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化(如果某个线程发生了未预期的Exception而结束,那么线程池会补充一个新的线程)
2、newCachedThreadPool:缓存线程池,创建一个可缓存的线程池,如果线程池的处理规模超过了当前处理需求时,那么将回收空闲的线程,而当需求增加时,添加新的线程,线程池的规模不存在任何限制。
3、newSingerThreadExecutor:单例线程执行者,是一个单线程的executor,它创建单个工作者线程来执行任务,如果这个线程异常结束,则会创建另一个线程来替代。同时他也能依然队列中任务的顺序来执行任务
4、newScheduledThreadPool:创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
并且,有关这几个线程池,我会采用关注创建工厂,以及executor方法还有列出父子关系等方式来观察分析他们的源码内容,而且,我们通过简单查看他们的构造工厂发现,其实他们都是实例化一个类ThreadPoolExecutor,他们与这个类实现密切,所以,文章会单独拧出这个类出来进行源码讲解
1、ThreadPoolExecutor
没看过源码的会觉得,这个类觉得陌生,那么请接着看
1.1、来源
话不多说,上四大常用线程池的构造工厂:
ThreadPoolExecutor同时满足四大常用线程池的要求,简单来说,他是这四个的爸爸
1.2、类关系
他继承自抽象类AbstractExecutorService,该抽象类没有抽象方法的,因此也能明白,它是概念上的抽象,文字上的抽象,而非一种代码上必要的抽象关系。
、
而AbstractExecutorService则是接口ExecutorService的实现,接口中有很多抽象方法
ExecutorService接口继承自Executor,而executor大家很熟悉了,就不多做介绍了
1.3、构造器
他的构造器有四个:
其实没什么特殊,都是调用参数最完整那个。而最少也要给出如下几个参数
最多则有那么多,也能看到可选的就是异常处理,还有线程工厂,由此多延伸出来三个构造器,只有异常处理或是只有线程工厂,以及两个都有的情况。也能看出,构造器并没有办什么实际的事儿,他就是把参数记录一下,
然后先来看看各个必须参数代表的含义
corePoolSize:默认线程池大小
maximumPoolSize:线程池可扩张的最大大小
keepAliveTime:官方解释是:Timeout in nanoseconds for idle threads waiting for work,解释一下,就是当一个新的线程进来,我们的线程池满了,不论是等待线程池扩张的时间,还是线程池到了最大值无法扩张,等待工作线程结束让出空位的时间,这两种时间都叫keepAliveTime
unit:这个unit,这里其实蛮尴尬的,只是一个用来转换keepAliveTime的工具用什么而已,从下图也能看出,并不实际存他
workQueue:顾名思义,他是用来管理工作线程的队列,其实现必须是阻塞队列
然后再来看看可选参数threadFactory,只是一个接口,要求具体实现,
来看看默认线程工厂的实现,defaultThreadFactory,是Executors工具类的一个内部类,请接着往下看他干了什么做什么的?
关于工厂DefaultThreadFactory,他有一个newThread方法,是一个工厂,专门负责创建我们当前线程池所需线程的,然后先注意一下这个工厂的构造器,他的group是取自Thread中成员变量group,用来标记每个线程属于哪个group的。
顺带一提newThread,会规范的生产线程,给定当前group,并且按照自己的规范取名。注明生产出的线程是非守护线程,以及默认优先级为5
然后再来看看可选参数handel,他直接用了类加载时准备阶段生成的AbortPolicy,关于这个类,后文会用到,暂时看看
他是ThreadPoolExecutor的一个内部类,其实现,只是为了抛异常,抛出规范化的异常,不会影响线程池运作的异常
1.4、executor方法
代码如下,首先粗略的看一下,根据方法名参数不难看出什么意思,先把大致代码意思说一下把,获取当前工作队列的长度,小于默认大小就直接插入。否则,就是超出默认大小,或者没有插入成功,对于超出默认大小的,第二个if保证,延时阻塞插入,延时插入能成功的话,判断有没有运行不正常的节点,不正常的移除,抛出异常。而当没有插入成功时,最后一个if保证,再试一次,还不行,ok抛出异常了。
如下几个方法源码是关键:ctl、workCountOf()方法、addWorker()、isRunning()、reject()、remove(),下面会一一作出解释
首先是ctl,只是一个原子的状态计数值,所以ctl.get()只是原子的获取一个状态值放入线程变量区,关于ctl的算法想要看明白,建议去刷《hackers delight》这里理解成神秘的算法,满足简单count++或是count--即可
关于workCountOf、reject、isRunning如下,实现如名字般简单容易理解,reject是说明,非运行状态下,无法从队列移除,那么就抛出异常。
remove方法:直截了当的移除,关于try方法这里就不截图做详细说明了,后面注释可以了解,当前为空时并且造成了关闭的处理,博主进去看了,很长,大概意思就是将全局阻塞的线程全部激活。
addWaiter方法在这里比较核心。他比较长,这里分段截图说明。
第一段自旋,主要是将workQueue的异常、正常状态,以某种算法和ctl联系在一起,异常下ctl为几,正常情况下ctl为几,然后需要注意一下的是core参数,图中wc >= (core ? corePoolSize : maximumPoolSize),也就是说这个参数是用来判断添加工作者的时候,ture就是用默认值判断大小,false就是用最大值判断大小,前文executor源码中有多处addWorker,后面的第二个参数有ture有false,看完这里,就能很好的明白逻辑了,当使用core添加失败,就用max试试,个人认为,这样设计不好,为了解耦,而让代码运行更加繁复,有点本末倒置了
随后这一段的数据准备,需要了解下Worker是什么。他是个满足了AQS的一种线程,ThreadPoolExecutor的一个内部类,这个类呢,就是我们常说的线程池中的工作线程,构造器传入一个线程工作,拓展了几种方法,设计模式上而言,是一种组合模式,也可以叫做装饰器模式,实际操作的还是构造器传入线程,
话题回到之前介绍的addWorker方法。一下是后续的所有代码,将线程传入工作线程,后面的代码也是一目了然,加上全局锁,将工作线程添加进工作线程。这里有个疑问,为什么要加全局锁,阻塞队列中add操作是线程安全的操作,其实,只是为了把添加操作和其他状态判断抛出异常处理给原子化。
然后是添加完毕后再判断,队列有没有超出设定,没有超出最大值的话,start启动。超出的话则使用失败处理addWorkerFailed,这里需要留意,线程池的线程数量的控制,并非使用阻塞队列的有界性抛出异常实现的,而是另外用了个计数器。
然后我们来看看超出线程池的部分怎么样?
移除,并且设置状态值ctl,而tryTerminate方法前面也提到过,当前为空时并且造成了关闭的处理。
看完这里,会产生一个疑问,那么如果超出线程队列默认大小时,又没达到最大怎么办?怎么扩容,我们忽略了一点,各个工具是分工合作得,我们这里采用了自身变量去记录队列大小,实际上,他是一个blockingQueue接口,具体实现,可能是Array的,可能时linked,也可能优先级、同步队列等等等,每个实际不同的实现一开始初始默认值本来就不同,扩容也不同,我们这个类关注的是线程交给工作线程去处理,以及管理线程的一些功能。既然选择了BlockingQueue,那么关于容量处理,完全可以放心的交给他去做。
2、newFixedThreadPool
混合线程池的构建如下图,他是依靠ThreadPoolExecutor中,默认大小和最大大小去固化线程池容量的。队列选择,LinkedBlockinQueue,默认大小Integer.MaxValue()。毕竟参数要求int,也不会有范围方面的错什么大小超过队列大小啥的,
3、newCachedThreadPool
不用传入参数,默认0,最大值Interger.MAX_VALUE,这跟之前不同是keepAliveTime不为0了,但是,考虑到keepAliveTime的官方解释,这里又是SynchronousQueue的链表实现,这玩意在这里,好像没啥用,而且博主看到这里也纳闷,返回去再看一下executor里面,也没用到这个参数,再结合缓存线程池的概念,吐槽一下,这参数给的,纯属好看。工作队列是SynchronousQueue实现的,关于这个容器的源码细节,可以参考我另外一篇博客https://blog.csdn.net/m0_37590135/article/details/101771074
其实关于介绍中说的Cache,解读了源码,我们可以发现,完全就是在唬人,说什么满了会回收没在运行线程,这玩意也不是他特有,fixed线程池同样也有,相信大家项目中也遇到过,面对甲方,产品,leader,或是造风产品会啥的,面对一个完全说不出优点的玩意,那就把大家都有的特点拿出来说一说造造势,也可以理解。
他与fixed线程池相比的唯一优点,可能就是阻塞队列的选择不同,Synchronous,完全采用CAS实现的Put、take,和linkedBlockingQueue对比,Linked是采用读写锁的分离形式。如果说优点,只是某些情况下Synchronous性能高于Linked,而另一些情况下相反
4、newSingerThreadExecutor
所谓的单例线程池,也不过是fixed混合线程池的默认最大都只给了1。这里没必要多做解释了
5、newScheduledThreadPool
延迟线程池,不管是哪种工厂,与之前三种不同的是,其都是创建ScheduledThreadPoolExecutor对象
然而他们本质上没有什么区别,因为他是继承自ThreadPoolExecutor的
5.1、RunnableScheduledFuture
5.1.1、来源
这个类是newScheduledThreadPool延迟线程池,中工作队列的载体,也即是下图这样得关系
5.1.2、类关系
首先他本是只是一个接口,继承自RunnableFuture和ScheduledFuture得一个接口,那么这俩又是个什么玩意呢?请接着往下看
首先是RunnableFuture如图,继承自Runnable和Future,回想一下FutureTask中,提到的Executors中一个Runnable和Callable得适配转换器,这里一样,RunnableFuture只是个适配器接口,提供Runnable和Future得相互转化
然后是ScheduledFuture,继承自Delayed和Future,也可知同样得套路,也是一个Delayed和Future得适配转换器
结论:推论出,RunnableScheduledFuture实际上同样是一个适配转换器,从名字就能记住,他提供Runnable、Delayed、Future之间得互相转换
5.2、DelayedWorkQueue
5.2.1、来源
他是newScheduledThreadPool延迟线程池中,工作队列的一个选择
5.2.2、类关系
从名字可以看出,他属于队列集合中的一种,同时是一种只能传入Runnable的阻塞队列
继承自抽象类,抽象队列
再继承自抽象集合,实现了集合接口
5.2.3、构造方法
他没有构造方法
他的所有参数的初始化并非在构造方法内实现的,是直接给的
5.2.4、offer方法
这里,不管是put还是add,都是调用offer方法,而offer入队方法很简单,出队入队加锁,锁是同一把锁,加锁入队操作,容量不足扩容,由上自下顺序看方法,grow扩容
关于扩容如此,扩容50%
然后setIndex,在每个ScheduledFutureTask中的头节点参数指定,而ScheduledFutureTask实际上是实现上文提及的RunnableScheduledFuture的并且继承了FutureTask,不用说,ScheduledFutureTask类就是在Runnable,Layed,Future的互相转换的基础上,去增加了Task的互相转换
然后是siteup,下图源码第一句,parent = (k - 1) >>> 1,这一句很熟悉啊,脑袋里面反应,堆排序,数组形式,所以刚刚提及的heapIndex就不难理解了,堆中索引,一个逻辑值,跟数组索引没有实际关系,所以总结一下,offer方法插入,实际就是一个加锁后堆排序插入罢了
5.2.4、take方法
如图,先不考虑其在线程池的应用,他既然是队列,就该以标准的生产者消费者模型去看待,首先加锁,队列为空,无从消费,那就阻塞,上文的offer方法也看到了,当队列插入的元素为第一个时,激活所有的阻塞线程,这里,会很纳闷,那么阻塞之后再激活,岂不是拿不到元素了?注意,这里for(),是自旋,自旋消费。剩下没什么好解读的了,就是延时时,使用阻塞
5.2、ScheduleThreadPoolExecutor
5.1.1、构造器
但是还是有些区别,再来从构造器开始看起,他对应于ThreadPoolExecutor,不一样的是,Max大小为固定Interger.MAX_VALUE,然后他的工作队列用的是DelayedWorkQueue,DelayedWorkQueue是一个BlockQueue,他同时也是ScheduleThreadPoolExecutor的一个内部类
5.1.2、executor方法
上文已经了解了ScheduleThreadPoolExecutor的隶属,以及它e的构造器,接下来看看他的关键方法execute
主要实现再schedule中,校验之类的跳过,主要关注decorateTask方法和delayedExecute方法,还有TriggerTime方法,首先使用ScheduleFutureTask这个转换类承载Runnable command,然后执行delayedExecute,
首先是decorateTask方法,好像很多余一个参数
然后是triggerTime方法,返回DelayedWorkQueue工作队列的根节点,上文提及了,他是堆排序的, 但这里跟他结构无关,主要是返回一个TimeUtil标准,delay大于long最大值时的一个特殊处理
最后是delayedExecute,这个类比较复杂,看似很短,首先关闭状态时抛出异常,就不用解释了。然后是,向延迟队列中添加任务,没有运行的话就移除,并且取消,在添加后无异常的话,来到ensurePrestart方法,初看很纳闷,向worker工作线程中,加入Null任务,咋一看不太明白是什么意思,但是想想就明白了,因为延迟线程池,实际上是继承自ThreadPoolExecutor的,所有的core大小,max大小都在父类中,addWorker空转,只是简单的看看此时的线程池中的delayed工作队列中的线程数量有没有超过core大小,有没有超过max大小,但实际上,这些都没啥用,ensurePrestart本身就做了这种判断了,请看下面的第二个图和第三个图,使用addWorker的唯一原因,只是为了更新神秘算法的状态值ctl。
所以他并不使用实际意义上的工作线程,Worker,要非要说他的工作线程的话,只能是delayedworkQueue中的,提供了Future、Runnable、Task、Delayed的语义转换的ScheduledFutureTask
6、Worker
Worker,就是指工作线程,我把它单独分成一个大节并非是说明他有多复杂,而是因为它的重要程度很高。也是贯穿线程池中的核心对象
6.1、来源
他是ThreadPoolExecutor的内部类,这样的结构来源,并不能说明它的重要性,这么说吧。
他是很多书本或是博客上提及的Executor框架中线程池的工作线程, 任务提交到工作线程指的就是他,也是大多数线程池的"任务载体",工作队列中承载的就是它。说了一堆有的没的 ,那么看看这玩意实际来源 。如果上面看的认真的话,这部分就可以跳过了
addWorker方法中,方法名就已经很直截了当了,添加Worker,随后是new 了一个Worker在满足条件的情况下添加
6.2、类关系
满足AQS的线程
6.3、构造器
看到firstTask,我的第一反应是,他会承载很多任务,自动调度执行,但是看了变量
变量官方说明,thread this worker is running in,Initial task to run,看了说明无法彻底明白他们的用途,这里暂且放下,有些变量是在使用中向用户展现它的价值的
6.4、run方法
它既然本质上是个线程,那么run一定是它的核心方法,这点毫无疑问。
但是这里只用了runWorker。我们通常所说的调度交给了线程池管理,其实这句就能很好的体现了
6.5、AQS相关方法
根据经验,AQS一般要override两个方法,一个是请求,一个是释放。
代码也是一目了然,就不赘述了,更改状态,以及独占模式的线程
6.5、runWorker方法
其实,这个方法并不在Worker中,他是ThreadPoolExecutor的方法,只不过Worker的run只用了这个方法,我们通过runWorker来探究工作线程的工作原理,才是正道,所以把它纳入Worker下说明,只是这个原因,它能很好的诠释Worker的行为
runWorker比较长,分段截图说明,首先是栈帧内初始化,我们了解到,上文提及的firestTask,是一次性的,得到后清空,但是关于无厘头的解锁,其实下文会接着解释,下文操作中会加锁,提前解锁是为了防止cpu中流水线任务分发带来的滞后问题,firstTask我已经拿到都处理好半天了,你还没置空,下一次操作还拿到未变的firstTask怎么办?简单来说,对于java人而言就是指令重排。
随后会循环取任务,而关于getTask也值得一看
getTask第一段,主要是更新当前ctl状态值的判断,以及更新全局ctl,然后有一段得注意,timed = allowCoreThreadTimeOut || wc > corePoolSize。他是布尔型,这里就决定了是否还等待,因为他是for循环,那么不管是timed还是timedOut都有可能改变
getTask第二段,timed决定了任务是延时入队,还是即时得,上文提到timed和core大小有关,也跟allow参数有关,而allow参数也跟keepalive参数有关,所以得到结论,是否延时入队,是keepalive参数决定得,和core默认大小决定的。由此可知,core默认大小和max最大大小,只是将队列砍成三段,第一段小于core时,即时take插入,第二段core和max之间的,poll延时插入,第三段,超过max的,不插入
看完getTask再总结和executor的方法的区别,即是,如同方法名一样。executor是在提交任务时候,对队列的操作,而runWorker是在运行中,对队列的操作。
然后回来接着看runWorker第二段,循环的第一段,根据ctl最近的位运算状态,判断是不是该中断了。
runWorker最后一段,beforeExecute见名知意,我们终于要开始正文了,执行前处理before内容,就如同awk命令,处理这一行前处理begin,处理完后处理end一样。之后,说实话,这一段代码写的很漂亮,个人很喜欢,直接运行task.run捕获异常抛出异常,之后如同beforeExecute一样,afterExceute,执行完后的动作。最终解锁,也看到了Worker中completedTasks是用于记录自己完成了多少任务的。
然后Work退出,processWorkerExit方法也看一下把
processWorkerExit方法,前面更新ctl就不说了。加锁。completedTaskCount是什么呢?相对于worker自身完成任务的计数,也要统计所有的Worker完成任务的计数。随后移除工作队列,解锁,完成。更新ctl什么的就不介绍了