ScheduledThreadPoolExecutor实现原理
自jdk1.5开始,Java开始提供ScheduledThreadPoolExecutor类来支持周期性任务的调度,在这之前,这些工作需要依靠Timer/TimerTask或者其它第三方工具来完成。但Timer有着不少缺陷,如Timer是单线程模式,调度多个周期性任务时,如果某个任务耗时较久就会影响其它任务的调度;如果某个任务出现异常而没有被catch则可能导致唯一的线程死掉而所有任务都不会再被调度。ScheduledThreadPoolExecutor解决了很多Timer存在的缺陷。
先来看看ScheduledThreadPoolExecutor的实现模型,它通过继承ThreadPoolExecutor来重用线程池的功能,里面做了几件事情:
- 为线程池设置了一个DelayedWorkQueue,该queue同时具有PriorityQueue(优先级大的元素会放到队首)和DelayQueue(如果队列里第一个元素的getDelay返回值大于0,则take调用会阻塞)的功能
- 将传入的任务封装成ScheduledFutureTask,这个类有两个特点,实现了java.lang.Comparable和java.util.concurrent.Delayed接口,也就是说里面有两个重要的方法:compareTo和getDelay。ScheduledFutureTask里面存储了该任务距离下次调度还需要的时间(使用的是基于System#nanoTime实现的相对时间,不会因为系统时间改变而改变,如距离下次执行还有10秒,不会因为将系统时间调前6秒而变成4秒后执行)。getDelay方法就是返回当前时间(运行getDelay的这个时刻)距离下次调用之间的时间差;compareTo用于比较两个任务的优先关系,距离下次调度间隔较短的优先级高。那么,当有任务丢进上面说到的DelayedWorkQueue时,因为它有DelayQueue(DelayQueue的内部使用PriorityQueue来实现的)的功能,所以新的任务会与队列中已经存在的任务进行排序,距离下次调度间隔短的任务排在前面,也就是说这个队列并不是先进先出的;另外,在调用DelayedWorkQueue的take方法的时候,如果没有元素,会阻塞,如果有元素而第一个元素的getDelay返回值大于0(前面说过已经排好序了,第一个元素的getDelay不会大于后面元素的getDelay返回值),也会一直阻塞。
- ScheduledFutureTask提供了一个run的实现,线程池执行的就是这个run方法。看看run的源码(本文的代码取自hotspot1.5.0_22,jdk后续版本的代码可能已经不一样了,如jdk1.7中使用了自己实现的DelayedWorkQueue,而不再使用PriorityQueue作为存储,不过从外面看它们的行为还是一样的,所以并不影响对ScheduledThreadPoolExecutor调度机制的理解):
publicvoidrun() {if(isPeriodic())runPeriodic();elseScheduledFutureTask.super.run();}如果不是周期性任务就直接执行任务(也就是else部分),这个主要是用于实现ScheduledThreadPoolExecutor#schedule(Callable callable, long delay, TimeUnit unit)和ScheduledThreadPoolExecutor#schedule(Runnable command, long delay, TimeUnit unit),后面会讲到它们的实现,这里先关注周期任务的执行方式。周期性任务执行的是runPeriodic(),看下它的实现:
privatevoidrunPeriodic() {booleanok = ScheduledFutureTask.super.runAndReset();booleandown = isShutdown();// Reschedule if not cancelled and not shutdown or policy allowsif(ok && (!down ||(getContinueExistingPeriodicTasksAfterShutdownPolicy() &&!isTerminating()))) {longp = period;if(p >0)time += p;elsetime = triggerTime(-p);ScheduledThreadPoolExecutor.super.getQueue().add(this);}// This might have been the final executed delayed// task. Wake up threads to check.elseif(down)interruptIdleWorkers();}这里可以看到,先执行了任务本身(ScheduledFutureTask.super.runAndReset),这个调用有一个返回值,来看下它的实现:
protectedbooleanrunAndReset() {returnsync.innerRunAndReset();}跟进去看下innerRunAndReset():
booleaninnerRunAndReset() {if(!compareAndSetState(0, RUNNING))returnfalse;try{runner = Thread.currentThread();callable.call();// don't set resultrunner =null;returncompareAndSetState(RUNNING,0);}catch(Throwable ex) {innerSetException(ex);returnfalse;}}可以发现,这里需要关注的是第三个return,也就是如果任务执行出现了异常,会被catch且返回false.
继续看runPeriodic()方法,if里面,如果刚才任务执行的返回值是true且线程池还在运行就在if块中的操作,如果线程池被关闭了就做else if里的操作。也就是说,如果之前的任务执行出现的异常返回了false,那么if里以及else if里的代码都不会执行了,那有什么影响?接下来看看if里做了什么。
if里的代码很简单,分为两部分,一是计算这个任务下次调度的间隔,二是将任务重新放回队列中。回到出现异常的情况,如果刚才的任务执行出现了异常,就不会将任务再放回队列中,换而言之,也就是这个任务再也得不到调度了!但是,这并不影响其它周期任务的调度。
综上,可以看到,ScheduledThreadPoolExecutor执行周期性任务的模型就是:调度一次任务,计算并设置该任务下次间隔,将任务放回队列中供线程池执行。这里的队列起了很大的作用,且有一些特点:距离下次调度间隔短的任务总是在队首,队首的任务若距离下次调度的间隔时间大于0就无法从该队列的take()方法中拿到任务。
接下来看看ScheduledThreadPoolExecutor#schedule(Callable callable, long delay, TimeUnit unit)和ScheduledThreadPoolExecutor#schedule(Runnable command, long delay, TimeUnit unit)这两个非周期性任务的实现方式,先看看它们的源码:
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) { if (command == null || unit == null) throw new NullPointerException(); ScheduledFutureTask<?> t = new ScheduledFutureTask<Boolean>(command, null, triggerTime(delay, unit)); delayedExecute(t); return t;} |
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) { if (callable == null || unit == null) throw new NullPointerException(); ScheduledFutureTask<V> t = new ScheduledFutureTask<V>(callable, triggerTime(delay, unit)); delayedExecute(t); return t;} |
private void delayedExecute(Runnable command) { if (isShutdown()) { reject(command); return; } // Prestart a thread if necessary. We cannot prestart it // running the task because the task (probably) shouldn't be // run yet, so thread will just idle until delay elapses. if (getPoolSize() < getCorePoolSize()) prestartCoreThread(); super.getQueue().add(command);} |
实现方式也很简单,在创建ScheduledThreadPoolExecutor内部任务(即ScheduledFutureTask)的时候就将调度间隔计算并设置好,如果当前线程数小于设置的核心线程数,就启动一个线程(可能是线程池刚启动里面还没有线程,也可能是里面的线程执行任务时挂掉了。如果线程池中的线程挂掉了而又没有调用这些schedule方法谁去补充挂掉的线程?不用担心,线程池自己会处理的)去监听队列里的任务,然后将任务放到队列里,在任务执行间隔不大于0的时候,线程就可以拿到这个任务并执行。
周期性任务的入口(ScheduledThreadPoolExecutor#scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)和ScheduledThreadPoolExecutor#scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit))与非周期性任务是类似的,它们处理方式不同的地方在于前文说到的ScheduledFutureTask#run()中。
ScheduledThreadPoolExecutor实现原理的更多相关文章
- 并发编程(十五)——定时器 ScheduledThreadPoolExecutor 实现原理与源码深度解析
在上一篇线程池的文章<并发编程(十一)—— Java 线程池 实现原理与源码深度解析(一)>中从ThreadPoolExecutor源码分析了其运行机制.限于篇幅,留下了Scheduled ...
- 并发编程(十四)—— ScheduledThreadPoolExecutor 实现原理与源码深度解析 之 DelayedWorkQueue
我们知道线程池运行时,会不断从任务队列中获取任务,然后执行任务.如果我们想实现延时或者定时执行任务,重要一点就是任务队列会根据任务延时时间的不同进行排序,延时时间越短地就排在队列的前面,先被获取执行. ...
- ScheduledThreadPoolExecutor源码解读
1. 背景 在之前的博文--ThreadPoolExecutor源码解读已经对ThreadPoolExecutor的实现原理与源码进行了分析.ScheduledExecutorService也是我们在 ...
- Java并发编程原理与实战三十八:多线程调度器(ScheduledThreadPoolExecutor)
在前面介绍了java的多线程的基本原理信息:线程池的原理与使用 本文对这个java本身的线程池的调度器做一个简单扩展,如果还没读过上一篇文章,建议读一下,因为这是调度器的核心组件部分. 我们如果要用j ...
- 线程池底层原理详解与源码分析(补充部分---ScheduledThreadPoolExecutor类分析)
[1]前言 本篇幅是对 线程池底层原理详解与源码分析 的补充,默认你已经看完了上一篇对ThreadPoolExecutor类有了足够的了解. [2]ScheduledThreadPoolExecut ...
- JUC回顾之-ScheduledThreadPoolExecutor底层实现原理和应用
项目中经常使用定时器,比如每隔一段时间清理下线过期的F码,或者应用timer定期查询MQ在数据库的配置,根据不同version实现配置的实时更新等等.但是timer是存在一些缺陷的,因为Timer在执 ...
- Java并发集合的实现原理
本文简要介绍Java并发编程方面常用的类和集合,并介绍下其实现原理. AtomicInteger 可以用原子方式更新int值.类 AtomicBoolean.AtomicInteger.AtomicL ...
- java多线程系类:JUC线程池:03之线程池原理(二)(转)
概要 在前面一章"Java多线程系列--"JUC线程池"02之 线程池原理(一)"中介绍了线程池的数据结构,本章会通过分析线程池的源码,对线程池进行说明.内容包 ...
- ScheduleThreadPoolExecutor的工作原理与使用示例
欢迎探讨,如有错误敬请指正 如需转载,请注明出处 http://www.cnblogs.com/nullzx/ 1. ScheduleExecutorService接口.ScheduledFuture ...
随机推荐
- 基于AT89C51单片机烟雾传感器
#include <reg51.h> #include <stdio.h> #define uchar unsigned char //宏定义无符号字符型 #define ui ...
- [ZJOI2013]K大数查询
Description: 给定一个序列,支持两种操作 1.在[L,R]的每个位置上加上一个数 (注意一个位置上有多个数) 2.查询[L,R]上所有数中的第K大 Hint: \(n,m<=5e4\ ...
- BZOJ4738 : 汽水
二分答案$mid$,若存在一条路径满足$|ave-k|<mid$,则答案至多为$mid-1$. 若$ave\leq k$,则$\sum(w-k)\leq 0$,且$\sum(k-w-mid)&l ...
- [P1373]小a和uim之大逃离 (DP)
[题目链接] 模拟赛的时候的一道题 因为老师不小心把数据发下来了……我考试打表的 考完之后Orz xzjds 然后开始打正解 题意 大概就是两个人,走矩阵,两个人各加上走上的矩阵的数值,要求最终两个人 ...
- 自定义simple_tag和filter在html中渲染出来的联系和区别
关于 simple_tag: 1,在app下创建一个(templatetags)目录,(被引用的模块必须放在该目录下,且目录名称不可更改): 2,创建任意py文件: 3,创建template对象: f ...
- Hibernate(10)_双向n对1(双向1对n)
1.双向 1-n 与 双向 n-1 是完全相同的两种情形,这里使用双向多对一来演示 双向 1-n 需要在 1 的一端可以访问 n 的一端, 反之依然. 出版社和图书的关系:Publishers--Bo ...
- Hibernate(7)关联关系_单向1对n
1.单向一对多(@OneToMany)关联是比较少用的(一般用双向一对多代替). 2.实体类: 1端:Publishers.java public class Publishers { private ...
- C#修改文件名方法
static void Main(string[] args) { string srcFileName = @"c:\order.txt"; string destFileNam ...
- 3D引擎为什么使用三角形绘制曲面
这个问题是我第一次接触3D开发就有的疑问,最近在看<游戏引擎架构>(Game Engine Architecture),在书中找到了答案. 三角网格(Triangle Mesh),游戏开发 ...
- Css3实现波浪效果3-静态波纹
一.外框宽度等比例3个椭圆拼合 .container { position: absolute; width: 400px; height: 200px; border: 5px solid rgb( ...