ScheduledThreadPoolExecutor中定时周期任务的实现源码分析
ScheduledThreadPoolExecutor是一个定时任务线程池,相比于ThreadPoolExecutor最大的不同在于其阻塞队列的实现
首先看一下其构造方法:
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}
ScheduledThreadPoolExecutor是继承自ThreadPoolExecutor的,可以看到这里实际上调用了ThreadPoolExecutor的构造方法,而最大的不同在于这里使用了默认的DelayedWorkQueue“阻塞队列”,这是后续能够实现定时任务的关键
在ScheduledThreadPoolExecutor中使用scheduleWithFixedDelay或者scheduleAtFixedRate方法来完成定时周期任务
以scheduleWithFixedDelay为例
scheduleWithFixedDelay方法:
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (delay <= 0)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(-delay));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
这里首先会将我们的任务包装成ScheduledFutureTask
(这里的delay在传入ScheduledFutureTask的构造方法时变为了负的,这是和scheduleAtFixedRate方法唯一不一样的地方)
ScheduledFutureTask方法:
private void delayedExecute(RunnableScheduledFuture<?> task) {
if (isShutdown())
reject(task);
else {
super.getQueue().add(task);
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
else
ensurePrestart();
}
}
这里不同于ThreadPoolExecutor中的处理,并没有考虑coreSize和maxSize和任务之间的关系,而是直接将任务提交到阻塞队列DelayedWorkQueue中
DelayedWorkQueue的add方法:
public boolean add(Runnable e) {
return offer(e);
} 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();
size = i + 1;
if (i == 0) {
queue[0] = e;
setIndex(e, 0);
} else {
siftUp(i, e);
}
if (queue[0] == e) {
leader = null;
available.signal();
}
} finally {
lock.unlock();
}
return true;
}
实际上调用了offer方法,从这里就可以看出这个“阻塞队列”的不同之处
DelayedWorkQueue中有这些成员:
private static final int INITIAL_CAPACITY = 16;
private RunnableScheduledFuture<?>[] queue =
new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
private int size = 0;
private Thread leader = null;
在DelayedWorkQueue内部维护的是queue这个初始大小16的数组,其实就是一个小根堆
回到offer方法
由于是在多线程环境,这里的操作使用了重入锁保证原子性
若是在size大于数组的长度情况下,就需要调用grow方法来扩容
grow方法:
private void grow() {
int oldCapacity = queue.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // grow 50%
if (newCapacity < 0) // overflow
newCapacity = Integer.MAX_VALUE;
queue = Arrays.copyOf(queue, newCapacity);
}
可以看到这是一个非常简单的扩容机制,申请一个1.5倍大小的新数组,再将原来的数据copy上去
回到offer方法,在调整完容量后,就需要进行数据的插入,使其形成一个小根堆
可以看到,在if-else判断中,首先检查是不是第一个元素,若是第一个,则直接放入数组,同时调用
setIndex方法,和任务关联
setIndex方法:
private void setIndex(RunnableScheduledFuture<?> f, int idx) {
if (f instanceof ScheduledFutureTask)
((ScheduledFutureTask)f).heapIndex = idx;
}
这个方法很简单,将下标关联到之前包装好的任务ScheduledFutureTask中
若不是第一个元素,则需要调用siftUp,进行小根堆的调整
siftUp方法:
private void siftUp(int k, RunnableScheduledFuture<?> key) {
while (k > 0) {
int parent = (k - 1) >>> 1;
RunnableScheduledFuture<?> e = queue[parent];
if (key.compareTo(e) >= 0)
break;
queue[k] = e;
setIndex(e, k);
k = parent;
}
queue[k] = key;
setIndex(key, k);
}
因为小根堆实际上就是一个二叉树,利用二叉树的性质根据当前要插入节点的下标,得到其父节点的下标parent ,再和父节点的RunnableScheduledFuture对象进行compareTo的比较(RunnableScheduledFuture继承了Comparable接口)
compareTo的实现:
public int compareTo(Delayed other) {
if (other == this) // compare zero if same object
return 0;
if (other instanceof ScheduledFutureTask) {
ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
long diff = time - x.time;
if (diff < 0)
return -1;
else if (diff > 0)
return 1;
else if (sequenceNumber < x.sequenceNumber)
return -1;
else
return 1;
}
long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
}
这里的逻辑比较简单,只需要看第二个if
在前面ScheduledFutureTask包装我们的任务的时候,其构造方法如下:
ScheduledFutureTask(Runnable r, V result, long ns, long period) {
super(r, result);
this.time = ns;
this.period = period;
this.sequenceNumber = sequencer.getAndIncrement();
}
这里的time 也就是initialDelay,period 就是-delay,sequenceNumber 是一个全局自增的序列号
那么在上面的compareTo方法中,就首先根据子节点的initialDelay和父节点的initialDelay比较
若是子节点小于父节点,返回-1,子节点大于父节点返回1
若是相等,则根据序列号比较,序列号小的返回-1
回到siftUp方法,通过compareTo方法,若是大于等于0,就说明子节点大于父节点,不需要做调整,结束循环
若是小于0,说明子节点小于父节点,那么就需要将父节点先交换到当前位置,再将k变成parent,在下一次循环时,就会找parent的parent,重复上述操作,直至构成小根堆
最后将要插入的节点放入queue中合适的位置
那么在后续的任务添加中,就会根据任务的initialDelay,以及创建时间,构建一个小根堆
回到offer方法,在小根堆中插入完节点后,若是第一次插入, 将leader(Thread对象)置为null,利用available(Condition对象)唤醒Lock 的AQS上的阻塞
DelayedWorkQueue的add到此结束,回到delayedExecute方法中,在完成向阻塞队列添加任务后,发现线程池中并没有一个worker在工作,接下来的工作就由ThreadPoolExecutor的ensurePrestart方法实现:
void ensurePrestart() {
int wc = workerCountOf(ctl.get());
if (wc < corePoolSize)
addWorker(null, true);
else if (wc == 0)
addWorker(null, false);
}
可以看到这里根据ctl的取值,与corePoolSize比较,调用了线程池的addWorker方法,那么实际上也就是通过这里开启了线程池的worker来进行工作
来看看在worker的轮询中发生了什么:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
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();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
可以看到在ThreadPoolExecutor的worker轮询线程中,会通过getTask方法,不断地从阻塞队列中获取任务
getTask方法:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out? for (;;) {
int c = ctl.get();
int rs = runStateOf(c); // Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
} int wc = workerCountOf(c); // Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
} try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
可以看到在这个方法中,在一系列的参数检查并设置完毕后,会通过workQueue的poll或者take方法来获取所需的任务
其中poll方法是在设置了超时时间的情况下进行获取,take则不带有超时时间
以take为例
DelayedWorkQueue的take方法:
public RunnableScheduledFuture<?> take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
RunnableScheduledFuture<?> first = queue[0];
if (first == null)
available.await();
else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
return finishPoll(first);
first = null; // don't retain ref while waiting
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && queue[0] != null)
available.signal();
lock.unlock();
}
}
在for循环中首先取出数组中的第一个元素,也就是生成的小根堆中最小的那一个
得到first后,若是first为null,则说明当前没有可执行的任务,则使用available这个Condition对象,将AQS阻塞起来,等待下次任务创建时再通过前面提到的available唤醒阻塞
若是first存在,则通过getDelay方法获取时间间隔
getDelay方法:
public long getDelay(TimeUnit unit) {
return unit.convert(time - now(), NANOSECONDS);
}
这个方法就是用time减去当前时间now,得到的一个纳秒级时间差值
而time是在ScheduledFutureTask执行构造方法时,通过triggerTime方法,使用initialDelay进行计算出来的
triggerTime方法:
private long triggerTime(long delay, TimeUnit unit) {
return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
} long triggerTime(long delay) {
return now() +
((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
} private long overflowFree(long delay) {
Delayed head = (Delayed) super.getQueue().peek();
if (head != null) {
long headDelay = head.getDelay(NANOSECONDS);
if (headDelay < 0 && (delay - headDelay < 0))
delay = Long.MAX_VALUE + headDelay;
}
return delay;
}
可以看到time在这里实际上就是通过initialDelay加上当时设置的纳秒级时间组成的
其中overflowFree是为了防止Long类型的溢出做了一次计算,后边再说
所以take方法中,通过getDelay方法得到的是一个时间差,若是时间差小于等于0,则说明任务到了该执行的时候了,此时调用finishPoll
finishPoll方法:
private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
int s = --size;
RunnableScheduledFuture<?> x = queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
setIndex(f, -1);
return f;
}
这个方法的逻辑还是比较简单的,就是一个简单的小根堆重新调整的操作,由于f需要被取出,此时利用最后一个元素,完成一次自上向下的调整(生成时是自下向上)
siftDown方法和siftUp类似:
private void siftDown(int k, RunnableScheduledFuture<?> key) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
RunnableScheduledFuture<?> c = queue[child];
int right = child + 1;
if (right < size && c.compareTo(queue[right]) > 0)
c = queue[child = right];
if (key.compareTo(c) <= 0)
break;
queue[k] = c;
setIndex(c, k);
k = child;
}
queue[k] = key;
setIndex(key, k);
}
由二叉树性质half 保证只操作到倒数第二层
在循环中,首先根据k(当前也就是根节点),得到其左右孩子的下标
若是右孩子存在,那么就用左孩子和右孩子比较,选出最下的哪一个作为child
若是右孩子不存在,则直接使用左孩子作为child
当选出child后,再和待插入的元素key比较
若是key小,则结束循环,直接将key插入k所在位置
若不是,则将当前child所在元素放在k所在位置,然后从child位置继续开始向下寻找,直到找到一个大于key或者遍历完毕
这样自上向下的将当前堆又调整成了小根堆,以后的定时周期任务都是以这种方式来调用的
看到这ScheduledThreadPoolExecutor的定时周期任务已经基本理解了,只不过还存在一个问题,当执行周期任务,会从小根堆取出,那么该任务下一次的执行时间何时更新到小根堆?
回到ThreadPoolExecutor的worker的runWorker方法中,在调用完getTask方法后,在进行完一系列完全检查后,会直接调用task的run方法,而此时的task是经过之前ScheduledFutureTask包装的
ScheduledFutureTask的run方法:
public void run() {
boolean periodic = isPeriodic();
if (!canRunInCurrentRunState(periodic))
cancel(false);
else if (!periodic)
ScheduledFutureTask.super.run();
else if (ScheduledFutureTask.super.runAndReset()) {
setNextRunTime();
reExecutePeriodic(outerTask);
}
}
若是设置了周期任务(period不为0),那么isPeriodic方法为true
逻辑上就会执行runAndReset方法,这个方法内部就会调用我们传入的Runnable的run方法,从而真正地执行我们的任务
在执行完毕后,可以看到调用了setNextRunTime方法
setNextRunTime方法:
private void setNextRunTime() {
long p = period;
if (p > 0)
time += p;
else
time = triggerTime(-p);
}
这里就很简单,利用当前time和period计算出下一次的time
由于scheduleWithFixedDelay和scheduleAtFixedRate之前所说的不一样之处,在这里就得到了体现
因为scheduleAtFixedRate的period是大于0的,所以scheduleAtFixedRate计算出来的时间间隔就是initialDelay + n*period的这种形式,那么其执行就会有固定的时间点,不过这还是要取决于任务的执行时间,若是任务的执行时间大于时间间隔,那么当上一次任务执行完毕,就会立刻执行,而不是等到时间点到了,若是任务的执行时间小于时间间隔,那么毫无疑问就需要等到时间点到了才执行下一次的任务
由于scheduleWithFixedDelay的period是小于0的,所以需要执行triggerTime
triggerTime方法:
long triggerTime(long delay) {
return now() +
((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}
可以看到若是不存在Long类型的溢出问题,那么下一次的时间就等于当前时间加时间间隔,所以说scheduleWithFixedDelay的不同之处在于其算上了任务的实际执行时间
若是存在Long类型的溢出问题时
在overflowFree中:
private long overflowFree(long delay) {
Delayed head = (Delayed) super.getQueue().peek();
if (head != null) {
long headDelay = head.getDelay(NANOSECONDS);
if (headDelay < 0 && (delay - headDelay < 0))
delay = Long.MAX_VALUE + headDelay;
}
return delay;
}
首先通过peek得到队列中的第一个元素,若是不存在,则直接返回delay
若是存在,通过getDelay得到headDelay
这里就会存在两情况
任务还没达到执行时间,则headDelay 大于零
任务达到执行时间,但却由于之前的任务还没执行完毕,遭到了延时,headDelay 小于0
所以这次的计算就是将headDelay这部分超时时间减去,以防止后续影响compareTo的比较,从而引起offer顺序的错误
(只不过这种情况正常不会遇见。。。)
在计算完成下一次的运行时间后
调用reExecutePeriodic方法:
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
if (canRunInCurrentRunState(true)) {
super.getQueue().add(task);
if (!canRunInCurrentRunState(true) && remove(task))
task.cancel(false);
else
ensurePrestart();
}
}
其中传入的这个task(outerTask)其实就是当前执行完毕的这个任务,
可以看到这里canRunInCurrentRunState成立的情况下,就会通过
getQueue得到阻塞队列,再次通过DelayedWorkQueue的add方法将其加入到小根堆中,只不过这时的time发生了变化
若是情况正常,则继续通过ThreadPoolExecutor的ensurePrestart方法,调度worker的工作
这样定时周期任务就能正常执行
ScheduledThreadPoolExecutor分析到此结束
ScheduledThreadPoolExecutor中定时周期任务的实现源码分析的更多相关文章
- java中的==、equals()、hashCode()源码分析(转载)
在java编程或者面试中经常会遇到 == .equals()的比较.自己看了看源码,结合实际的编程总结一下. 1. == java中的==是比较两个对象在JVM中的地址.比较好理解.看下面的代码: ...
- Vue3中的响应式对象Reactive源码分析
Vue3中的响应式对象Reactive源码分析 ReactiveEffect.js 中的 trackEffects函数 及 ReactiveEffect类 在Ref随笔中已经介绍,在本文中不做赘述 本 ...
- Java并发包中Semaphore的工作原理、源码分析及使用示例
1. 信号量Semaphore的介绍 我们以一个停车场运作为例来说明信号量的作用.假设停车场只有三个车位,一开始三个车位都是空的.这时如果同时来了三辆车,看门人允许其中它们进入进入,然后放下车拦.以后 ...
- 详解SpringMVC中Controller的方法中参数的工作原理[附带源码分析]
目录 前言 现象 源码分析 HandlerMethodArgumentResolver与HandlerMethodReturnValueHandler接口介绍 HandlerMethodArgumen ...
- 【MVC - 参数原理】详解SpringMVC中Controller的方法中参数的工作原理[附带源码分析]
前言 SpringMVC是目前主流的Web MVC框架之一. 如果有同学对它不熟悉,那么请参考它的入门blog:http://www.cnblogs.com/fangjian0423/p/spring ...
- 【小家Spring】聊聊Spring中的数据绑定 --- DataBinder本尊(源码分析)
每篇一句 唯有热爱和坚持,才能让你在程序人生中屹立不倒,切忌跟风什么语言或就学什么去~ 相关阅读 [小家Spring]聊聊Spring中的数据绑定 --- 属性访问器PropertyAccessor和 ...
- RocketMQ中PullConsumer的消息拉取源码分析
在PullConsumer中,有关消息的拉取RocketMQ提供了很多API,但总的来说分为两种,同步消息拉取和异步消息拉取 同步消息拉取以同步方式拉取消息都是通过DefaultMQPullConsu ...
- 2、JDK8中的HashMap实现原理及源码分析
本篇提纲.png 本篇所述源码基于JDK1.8.0_121 在写上一篇线性表的文章的时候,笔者看的是Android源码中support24中的Java代码,当时发现这个ArrayList和Linked ...
- JDK1.8中LinkedList的实现原理及源码分析
详见:https://blog.csdn.net/cb_lcl/article/details/81222394 一.概述 LinkedList底层是基于双向链表(双向链表的特点, ...
随机推荐
- Android 开源库StickyListHeadersListView来实现ListView列表分组效果
项目中有一新的需求,要求能像一些Android机带"联系人列表"一样,数据可以自动分组,且在列表滑动过程中,列表头固定在顶部,效果图如下: 下面就带大家实现上面的效果, 首先,我们 ...
- JIRA管理员、用户密码重置
-- Jira数据库中,用户信息都存放在表 cwd_user中 -- 切换到jiar数据库 use jiradb; -- 更改密码为sphere update cwd_user set credent ...
- Facebook 发布深度学习工具包 PyTorch Hub,让论文复现变得更容易
近日,PyTorch 社区发布了一个深度学习工具包 PyTorchHub, 帮助机器学习工作者更快实现重要论文的复现工作.PyTorchHub 由一个预训练模型仓库组成,专门用于提高研究工作的复现性以 ...
- Wood Processing牛客第十场 斜率优化DP
卧槽我感觉写的是对的,但是就是样例都过不了...留坑 #include<iostream> #include<stdio.h> #include<string.h> ...
- oracle 用EXISTS替换DISTINCT
当提交一个包含一对多表信息(比如部门表和雇员表)的查询时,避免在SELECT子句中使用DISTINCT. 一般可以考虑用EXIST替换 例如: 低效: SELECT DISTINCT DEPT_NO, ...
- 全面解读Python Web开发框架Django,利用Django构建web应用及其部署
全面解读Python Web开发框架Django Django是一个开源的Web应用框架,由Python写成.采用MVC的软件设计模式,主要目标是使得开发复杂的.数据库驱动的网站变得简单.Django ...
- [转]移动APP安全测试
1 移动App安全风险分析 1.1 安全威胁分析 安全威胁从三个不同环节进行划分,主要分为客户端威胁.数据传输端威胁和服务端的威胁. 1.2 面临的主要风险 1.3 Android测试思维 ...
- 洛谷P2258 子矩阵 题解 状态压缩/枚举/动态规划
作者:zifeiy 标签:状态压缩.枚举.动态规划 题目链接:https://www.luogu.org/problem/P2258 这道题目状态压缩是肯定的,我们需要用二进制来枚举状态. 江湖上有一 ...
- 高级教程: 作出动态决策和 Bi-LSTM CRF 重点
动态 VS 静态深度学习工具集 Pytorch 是一个 动态 神经网络工具包. 另一个动态工具包的例子是 Dynet (我之所以提这个是因为使用 Pytorch 和 Dynet 是十分类似的. 如果你 ...
- vue echarts引用
<template> <!--为echarts准备一个具备大小的容器dom--> <div id="main" style="width: ...