最近新接手的项目里大量使用了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. 201521123020 《Java程序设计》第3周学习总结

    本周学习总结 初学面向对象,会学习到很多碎片化的概念与知识.尝试学会使用思维导图将这些碎片化的概念.知识组织起来.请使用纸笔或者下面的工具画出本周学习到的知识点.截图或者拍照上传. 书面作业 1.代码 ...

  2. Myeclipse设置文件编码

    设置选项分析: 1.window-->preferences-->General-->workspace-->Text file encoding 这里设置全局默认的文件编码格 ...

  3. 201521123013 《Java程序设计》第13周学习总结

    1. 本周学习总结 2. 书面作业 Q1. 网络基础 1.1 比较ping www.baidu.com与ping cec.jmu.edu.cn,分析返回结果有何不同?为什么会有这样的不同? ping值 ...

  4. 201521123024 《Java程序设计》 第九周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结异常相关内容. 2. 书面作业 本次PTA作业题集异常 1.常用异常 题目5-1 1.1 截图你的提交结果(出现学号) 1.2 自己 ...

  5. 数据库系统概论——Chap. 1 Introduction

    数据库系统概论--Introduction 一.数据库的4个基本概念 数据(data):数据是数据库中存储的基本单位.我们把描述事物的符号记录称为数据.数据和关于数据的解释是不可分的,数据的含义称为数 ...

  6. 多线程:多线程设计模式(二):Future模式

    一.什么是Future模型: 该模型是将异步请求和代理模式联合的模型产物.类似商品订单模型.见下图: 客户端发送一个长时间的请求,服务端不需等待该数据处理完成便立即返回一个伪造的代理数据(相当于商品订 ...

  7. eclipse复制到IDEA中文不匹配,编译失败

    今天使用把eclipse的包复制到Intellij Idea下,结果在编译的时候,它说我的数据是GBK,而Idea默认的数据是UTF-8,因此出错了... 解决:在项目中直接把对象的encoding. ...

  8. java基础知识6-- 抽象类,抽象方法,接口,构造方法,类方法等易混淆的知识点

    一.抽象类和抽象方法 (B 继承  抽象类A) 抽象类:有抽象方法的一定是抽象类 抽象方法:方法名前有abstract修饰,且方法没有方法体,即{},不需要实现任何功能,只是声明 1.抽象类中的方法有 ...

  9. 工作总结--如何定位web系统前后台的bug,以及bug分析/测试感想

    对于web项目前台和后台bug定位分析:一. 系统整体了解 懒企鹅营销服务平台用的架构:web前端: Bootstrap 3.0 组件丰富,兼容性好,界面美观 Server端: jsp+Servlet ...

  10. 举例让抽象问题具体化:包含min函数的栈

    定义栈的数据结构,请在该类型中实现一个能够得到栈最小元素的min函数.在该栈中,调用min.push及pop的时间复杂度都是O(1). import java.util.Stack; public c ...