最近新接手的项目里大量使用了ScheduledThreadPoolExecutor类去执行一些定时任务,之前一直没有机会研究这个类的源码,这次趁着机会好好研读一下。

原文地址:http://www.jianshu.com/p/18f4c95aca24

该类主要还是基于ThreadPoolExecutor类进行二次开发,所以对Java线程池执行过程还不了解的同学建议先看看我之前的文章。

当面试官问线程池时,你应该知道些什么?

一、执行流程

  1. 与ThreadPoolExecutor不同,向ScheduledThreadPoolExecutor中提交任务的时候,任务被包装成ScheduledFutureTask对象加入延迟队列并启动一个woker线程。

  2. 用户提交的任务加入延迟队列时,会按照执行时间进行排列,也就是说队列头的任务是需要最早执行的。而woker线程会从延迟队列中获取任务,如果已经到了任务的执行时间,则开始执行。否则阻塞等待剩余延迟时间后再尝试获取任务。

  3. 任务执行完成以后,如果该任务是一个需要周期性反复执行的任务,则计算好下次执行的时间后会重新加入到延迟队列中。

二、源码深入分析

首先看下ScheduledThreadPoolExecutor类的几个构造函数:

    public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
} public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory);
} public ScheduledThreadPoolExecutor(int corePoolSize,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), handler);
} public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}

注:这里构造函数都是使用super,其实就是ThreadPoolExecutor的构造函数

这里有三点需要注意:

  1. 使用DelayedWorkQueue作为阻塞队列,并没有像ThreadPoolExecutor类一样开放给用户进行自定义设置。该队列是ScheduledThreadPoolExecutor类的核心组件,后面详细介绍。
  2. 这里没有向用户开放maximumPoolSize的设置,原因是DelayedWorkQueue中的元素在大于初始容量16时,会进行扩容,也就是说队列不会装满,maximumPoolSize参数即使设置了也不会生效。
  3. worker线程没有回收时间,原因跟第2点一样,因为不会触发回收操作。所以这里的线程存活时间都设置为0。

再次说明:上面三点的理解需要先了解ThreadPoolExecutor的知识点。

当我们创建出一个调度线程池以后,就可以开始提交任务了。这里依次分析一下三个常用API的源码:

首先是schedule方法,该方法是指任务在指定延迟时间到达后触发,只会执行一次。

    public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit) {
//参数校验
if (command == null || unit == null)
throw new NullPointerException();
//这里是一个嵌套结构,首先把用户提交的任务包装成ScheduledFutureTask
//然后在调用decorateTask进行包装,该方法是留给用户去扩展的,默认是个空方法
RunnableScheduledFuture<?> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null,
triggerTime(delay, unit)));
//包装好任务以后,就进行提交了
delayedExecute(t);
return t;
}

重点看一下提交任务的源码:

    private void delayedExecute(RunnableScheduledFuture<?> task) {
//如果线程池已经关闭,则使用拒绝策略把提交任务拒绝掉
if (isShutdown())
reject(task);
else {
//与ThreadPoolExecutor不同,这里直接把任务加入延迟队列
super.getQueue().add(task);
//如果当前状态无法执行任务,则取消
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
else
//这里是增加一个worker线程,避免提交的任务没有worker去执行
//原因就是该类没有像ThreadPoolExecutor一样,woker满了才放入队列
ensurePrestart();
}
}

这里的关键点其实就是super.getQueue().add(task)行代码,ScheduledThreadPoolExecutor类在内部自己实现了一个基于堆数据结构的延迟队列。add方法最终会落到offer方法中,一起看下:

        public boolean offer(Runnable x) {
//参数校验
if (x == null)
throw new NullPointerException();
RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
final ReentrantLock lock = this.lock;
lock.lock();
try {
//查看当前元素数量,如果大于队列长度则进行扩容
int i = size;
if (i >= queue.length)
grow();
//元素数量加1
size = i + 1;
//如果当前队列还没有元素,则直接加入头部
if (i == 0) {
queue[0] = e;
//记录索引
setIndex(e, 0);
} else {
//把任务加入堆中,并调整堆结构,这里就会根据任务的触发时间排列
//把需要最早执行的任务放在前面
siftUp(i, e);
}
//如果新加入的元素就是队列头,这里有两种情况
//1.这是用户提交的第一个任务
//2.新任务进行堆调整以后,排在队列头
if (queue[0] == e) {
//这个变量起优化作用,后面说
leader = null;
//加入元素以后,唤醒worker线程
available.signal();
}
} finally {
lock.unlock();
}
return true;
}

通过上面的逻辑,我们把提交的任务成功加入到了延迟队列中,前面说了加入任务以后会开启一个woker线程,该线程的任务就是从延迟队列中不断取出任务执行。这些都是跟ThreadPoolExecutor相同的,我们看下从该延迟队列中获取元素的源码:

        public RunnableScheduledFuture<?> take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
//取出队列中第一个元素,即最早需要执行的任务
RunnableScheduledFuture<?> first = queue[0];
//如果队列为空,则阻塞等待加入元素时唤醒
if (first == null)
available.await();
else {
//计算任务执行时间,这个delay是当前时间减去任务触发时间
long delay = first.getDelay(NANOSECONDS);
//如果到了触发时间,则执行出队操作
if (delay <= 0)
return finishPoll(first);
first = null;
//这里表示该任务已经分配给了其他线程,当前线程等待唤醒就可以
if (leader != null)
available.await();
else {
//否则把给任务分配给当前线程
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
//当前线程等待任务剩余延迟时间
available.awaitNanos(delay);
} finally {
//这里线程醒来以后,什么时候leader会发生变化呢?
//就是上面的添加任务的时候
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
//如果队列不为空,则唤醒其他woker线程
if (leader == null && queue[0] != null)
available.signal();
lock.unlock();
}
}

这里为什么会加入一个leader变量来分配阻塞队列中的任务呢?原因是要减少不必要的时间等待。比如说现在队列中的第一个任务1分钟后执行,那么用户提交新的任务时会不断的加入woker线程,如果新提交的任务都排在队列后面,也就是说新的woker现在都会取出这第一个任务进行执行延迟时间的等待,当该任务到触发时间时,会唤醒很多woker线程,这显然是没有必要的。

当任务被woker线程取出以后,会执行run方法,由于此时任务已经被包装成了ScheduledFutureTask对象,那我们来看下该类的run方法:

        public void run() {
boolean periodic = isPeriodic();
//如果当前线程池已经不支持执行任务,则取消
if (!canRunInCurrentRunState(periodic))
cancel(false);
else if (!periodic)
//如果不需要周期性执行,则直接执行run方法然后结束
ScheduledFutureTask.super.run();
else if (ScheduledFutureTask.super.runAndReset()) {
//如果需要周期执行,则在执行完任务以后,设置下一次执行时间
setNextRunTime();
//把任务重新加入延迟队列
reExecutePeriodic(outerTask);
}
}

上面就是schedule方法完整的执行过程。

ScheduledThreadPoolExecutor类中关于周期性执行的任务提供了两个方法scheduleAtFixedRate跟scheduleWithFixedDelay,一起看下区别。

    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
//删除不必要的逻辑,重点看区别
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
//二者唯一区别
unit.toNanos(period));
//...
} public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit) {
//...
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
//二者唯一区别
unit.toNanos(-delay));
//..
}

前者把周期延迟时间传入ScheduledFutureTask中,而后者却设置成负数传入,区别在哪里呢?看下当任务执行完成以后的收尾工作中设置任务下次执行时间的方法setNextRunTime源码:

        private void setNextRunTime() {
long p = period;
//大于0是scheduleAtFixedRate方法,表示执行时间是根据初始化参数计算的
if (p > 0)
time += p;
else
//小于0是scheduleWithFixedDelay方法,表示执行时间是根据当前时间重新计算的
time = triggerTime(-p);
}

也就是说当使用scheduleAtFixedRate方法提交任务时,任务后续执行的延迟时间都已经确定好了,分别是initialDelay,initialDelay + period,initialDelay + 2 * period以此类推。

而调用scheduleWithFixedDelay方法提交任务时,第一次执行的延迟时间为initialDelay,后面的每次执行时间都是在前一次任务执行完成以后的时间点上面加上period延迟执行。

三、总结

ScheduledThreadPoolExecutor可以说是在ThreadPoolExecutor上面进行了一些扩展操作,它只是重新包装了任务以及阻塞队列。该类的阻塞队列DelayedWorkQueue是基于堆去实现的,本文没有太详细介绍堆结构插入跟删除数据的调整工作,感兴趣的同学可以私信或者评论交流。

Java调度线程池ScheduledThreadPoolExecutor源码分析的更多相关文章

  1. java线程池ThreadPoolExector源码分析

    java线程池ThreadPoolExector源码分析 今天研究了下ThreadPoolExector源码,大致上总结了以下几点跟大家分享下: 一.ThreadPoolExector几个主要变量 先 ...

  2. Java并发包源码学习系列:线程池ScheduledThreadPoolExecutor源码解析

    目录 ScheduledThreadPoolExecutor概述 类图结构 ScheduledExecutorService ScheduledFutureTask FutureTask schedu ...

  3. [转载] Java线程池框架源码分析

    转载自http://www.linuxidc.com/Linux/2014-11/108791.htm 相关类Executor,Executors,AbstractExecutorService,Ex ...

  4. Java核心复习——线程池ThreadPoolExecutor源码分析

    一.线程池的介绍 线程池一种性能优化的重要手段.优化点在于创建线程和销毁线程会带来资源和时间上的消耗,而且线程池可以对线程进行管理,则可以减少这种损耗. 使用线程池的好处如下: 降低资源的消耗 提高响 ...

  5. 线程池ThreadPoolExecutor源码分析

    在阿里编程规约中关于线程池强制了两点,如下: [强制]线程资源必须通过线程池提供,不允许在应用中自行显式创建线程.说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源 ...

  6. Python线程池ThreadPoolExecutor源码分析

    在学习concurrent库时遇到了一些问题,后来搞清楚了,这里记录一下 先看个例子: import time from concurrent.futures import ThreadPoolExe ...

  7. ThreadPoolExecutor(线程池)源码分析

    1. 常量和变量 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); // 高3位为线程池的运行状态,低29 ...

  8. 深入浅出Java线程池:源码篇

    前言 在上一篇文章深入浅出Java线程池:理论篇中,已经介绍了什么是线程池以及基本的使用.(本来写作的思路是使用篇,但经网友建议后,感觉改为理论篇会更加合适).本文则深入线程池的源码,主要是介绍Thr ...

  9. quartz集群调度机制调研及源码分析---转载

    quartz2.2.1集群调度机制调研及源码分析引言quartz集群架构调度器实例化调度过程触发器的获取触发trigger:Job执行过程:总结:附: 引言 quratz是目前最为成熟,使用最广泛的j ...

随机推荐

  1. 201521123010 《Java程序设计》第2周学习总结

    1. 本周学习总结 这周学习了在JAVA里各种数据类型的使用.各种运算符的使用.表达是的使用,还初步学习了枚举的用法,也掌握了一些枚举和switch语句结合的用法,还了解了一些字符串类.在实验课上也学 ...

  2. 201521123006 《java程序设计》 第14周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多数据库相关内容. 2. 书面作业 1. MySQL数据库基本操作 建立数据库,将自己的姓名.学号作为一条记录插入.(截图,需出现自 ...

  3. cocos2dx 播放gif

    起因 或许有人会说,cocos2dx中直接帧动画就行了用什么GIF. 起因是为游戏内部要用到第三方平台的头像,而第三方平台的头像大多都是用到Gif,所以才会有了这个需求 过程 查了各种文档都没找到.但 ...

  4. [实战演练]python3使用requests模块爬取页面内容

    本文摘要: 1.安装pip 2.安装requests模块 3.安装beautifulsoup4 4.requests模块浅析 + 发送请求 + 传递URL参数 + 响应内容 + 获取网页编码 + 获取 ...

  5. KMP算法的来龙去脉

    1. 引言 字符串匹配是极为常见的一种模式匹配.简单地说,就是判断主串TT中是否出现该模式串PP,即PP为TT的子串.特别地,定义主串为T[0-n−1]T[0-n−1],模式串为P[0-p−1]P[0 ...

  6. 微服务~Eureka实现的服务注册与发现及服务之间的调用

    微服务里一个重要的概念就是服务注册与发现技术,当你有一个新的服务运行后,我们的服务中心可以感知你,然后把加添加到服务列表里,然后当你死掉后,会从服务中心把你移除,而你作为一个服务,对其它服务公开的只是 ...

  7. C++Builder中MessageBox的基本用法

    C++Builder中MessageBox的基本用法 返回值:IDYES=Application->MessageBox("","",MBYESNO) i ...

  8. ssh (免密码登录、开启服务)

    ssh 无密码登录要使用公钥与私钥.linux下可以用用ssh-keygen生成公钥/私钥对,下面我以Unbutun为例.有机器A(192.168.1.155),B(192.168.1.181).现想 ...

  9. Spring-Boot:Spring Cloud构建微服务架构

    概述: 从上一篇博客<Spring-boot:5分钟整合Dubbo构建分布式服务> 过度到Spring Cloud,我们将开始学习如何使用Spring Cloud 来搭建微服务.继续采用上 ...

  10. 使用http -server 搭建本地简易文件服务器

    安装 npm install http-server -g 使用 1. cd project . 2. hs [pwd] -o, 默认是当前路径 ./ 3. 其他选项 -p Port to use ( ...