JUC源码分析-线程池篇(三)Timer
JUC源码分析-线程池篇(三)Timer
Timer 是 java.util 包提供的一个定时任务调度器,在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次。
1. Timer 类结构
Timer 由 TimerThread,TaskQueue ,TimerTask 组成。 Timer 初始化时会构建一个 TimerThread 线程用于执行调度任务。TaskQueue 保存所有调度的定时任务,基于二叉树小顶堆时间,第一个要执行的任务永远位于 queue[1],其插入和查找时间复杂度都是 log2n。
1.1 TimerTask 生命周期
int state = VIRGIN; // 任务状态
static final int VIRGIN = 0; // 初始化
static final int SCHEDULED = 1; // 任务已经调度,还未执行
static final int EXECUTED = 2; // 任务已经执行
static final int CANCELLED = 3; // 任务取消
VIRGIN 表示 Task 刚刚被创建,SCHEDULED 表示 Task 已经被加入 TaskQueue中,等待调度,EXECUTED 表示 Task 已经被执行,CANCELLED 表示 Task 已经被取消。
TimerTask 还有两个重要属性:
- nextExecutionTime 表示任务下一次的执行时间;
- period 表示任务调度的周期。0 表示只执行一次,1 表示 scheduleAtFixedRate 周期性定时任务,-1 表示 schedule 周期性定时任务。
1.2 Timer 定时调度函数
schedule(TimerTask task, Date time)
绝对时间执行一次。schedule(TimerTask task, long delay)
相对时间执行一次。schedule(TimerTask task, long delay, long period)
相对时间,每隔 period 重复执行。schedule 下次执行时间为当前时间 + period,很明显如果出现任务执行时间过长,会出现任务丢失,但不会造成线程阻塞。scheduleAtFixedRate(TimerTask task, long delay, long period)
相对时间,每隔 period 重复执行。scheduleAtFixedRate 下次执行时间为 task.nextExecutionTime + period,也就是上一次执行结束的时间,此时如果出现任务执行时间过长,任务不会丢失,但会造成线程阻塞。
前两个就不再解释了,schedule 和 scheduleAtFixedRate 这两个方法都是任务调度方法,他们之间区别是:schedule 会丢弃延迟的任务,不会造成线程阻塞,scheduleAtFixedRate 严格按 period 执行,不会丢弃任务,但都延迟执行了,造成了严重的阻塞。 Timer 内部是单线程执行,所以 Timer 调度的任务都不应该有阻塞。
举个例子:每 5s 调度一次,那么正常就是 0,5,10,15,20s,而任务调度耗时 10s。schedule 会变成 0,10,20s 执行,原本 5 次任务最后只执行了 3 次,不会阻塞,而 scheduleAtFixedRate 就会变成 0,10,20,30,40s,任务数量没有减少,但都延迟执行了,造成了严重的阻塞。
2. Timer 源码分析
2.1 TaskQueue
顾名思义,TaskQueue 就是用来保存 TimerTask 的队列。需要注意的是,TaskQueue 的内部实现使用的是最小堆,堆顶的 Task 是最近即将到时间的 Task,所以在调度任务时,每次只需要取出堆顶元素,判断时间是否已到即可,效率非常高。下面是 TaskQueue 的核心代码,其实就是最小堆的实现代码:
(1) 椎顶永远都是最先执行的定时任务
// 小顶椎椎顶永远都是最先执行的定时任务。但为什么是 1 而不从开始呢?以后有机会再研究下
TimerTask getMin() {
return queue[1];
}
// 定时调度后重新设置小顶堆
void rescheduleMin(long newTime) {
queue[1].nextExecutionTime = newTime;
fixDown(1);
}
// 每取走一个椎顶的定时任务,都需要重新将 queue[1]
void removeMin() {
queue[1] = queue[size];
queue[size--] = null; // Drop extra reference to prevent memory leak
fixDown(1); // 重新设置堆顶,保证堆顶的任务一定是最先执行的
}
(2) 添加定时任务 add
// 添加定时任务
void add(TimerTask task) {
// Grow backing store if necessary
if (size + 1 == queue.length)
queue = Arrays.copyOf(queue, 2*queue.length);
queue[++size] = task;
fixUp(size); // 重新设置堆顶,保证堆顶的任务一定是最先执行的
}
可以看到无论是添加还是删除都会重新设置堆顶的定时任务,保证椎顶永远都是最先执行的定时任务。
(3) 小顶堆数据结构
TaskQueue 数据结构的操作都是 fixUp 和 fixDown,查找和添加的时间复杂度都是 log2n。
// 元素上浮,只要比父节点小就交换位置
private void fixUp(int k) {
while (k > 1) {
int j = k >> 1;
if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
break;
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
k = j;
}
}
// 元素下浮,只要比左右子节点大就交换位置。因为子节点有两个,所以比较了两次
private void fixDown(int k) {
int j;
while ((j = k << 1) <= size && j > 0) {
if (j < size &&
queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)
j++; // j indexes smallest kid
if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)
break;
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
k = j;
}
}
// 将一个普通的数组设置为小顶堆
void heapify() {
for (int i = size/2; i >= 1; i--)
fixDown(i);
}
2.2 TimerThread
同样地,顾名思义,TimerThread 就是用来调度 TaskQueue 中的任务的线程。毫无疑问,TimerThread 肯定每次都会调度 queue.getMin() 的任务(堆顶),如果定时任务取消或已执行后就需要重新设置堆顶任务,保证堆顶永远是最先执行的定时任务。核心逻辑如下:
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// 1. 调用 cancel 方法后 newTasksMayBeScheduled=false。
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die
// 2. 小顶堆永远是最先执行的定时任务
long currentTime, executionTime;
task = queue.getMin();
synchronized(task.lock) {
// 3. 任务取消,重新设置堆顶任务
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
// 4. taskFired=true 表示任务已经执行时间到了,重新设置下一次的执行时间
if (taskFired = (executionTime<=currentTime)) {
if (task.period == 0) { // Non-repeating, remove
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
// 5. 任务需要重新排序
queue.rescheduleMin(
task.period<0 ? currentTime - task.period // schedule
: executionTime + task.period); // scheduleAtFixedRate
}
}
}
// 6. 还未到执行时间,限时等待。正常情况下不会被唤醒,除了①取消定时器,②添加新的定时任务
if (!taskFired) // Task hasn't yet fired; wait
queue.wait(executionTime - currentTime);
}
// 7. 任务执行时间到了,开始执行
if (taskFired) // Task fired; run it, holding no locks
task.run();
} catch(InterruptedException e) {
}
}
}
总结:TimerThread 会起一个 while 循环,从 queue 中取出当前最近即将到时间的 Task,然后判断 Task 的执行时间是否已经到了,如果还没到,则计算目标调度时间和当前时间的差值 delta,继续 wait delta 毫秒。wait 时间到之后会结束本次循环,在下一次循环中,如果没有新的更早的 task 加入,则当前的 task 将会被执行,同时设置下一次的执行时间。
2.3 Timer
从上面的介绍可知,TimerThread 的调度核心是起一个 while 循环,不断检查是否有 task 需要执行,其中两次调用了 queue.wait() 方法。那在哪些情况下 queue.notify() 方法会被调用呢?
2.3.1 任务调度
// schedule 会丢弃延迟的任务。下面的例子中 5 个任务会实际只会执行 3 个
// 比如任务原本该 0,5,10,15,20s 执行,但任务执行实际耗时 10s,实际执行时间 0,10,20
public void schedule(TimerTask task, long delay, long period) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, System.currentTimeMillis()+delay, -period);
}
// scheduleAtFixedRate 则不会丢弃延迟的任务,同样的例了任务都不会丢弃,但所有的任务都是延迟执行的。
// 比如任务原本该 0,5,10,15,20s 执行,但任务执行实际耗时 6s,实际执行时间 0,10,20,30,40
public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, System.currentTimeMillis()+delay, period);
}
schedule 和 scheduleAtFixedRate 都是调用 sched 完成任务入队的。
private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");
// Constrain value of period sufficiently to prevent numeric
// overflow while still being effectively infinitely large.
if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;
// 定时任务入队
synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");
synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}
// 添加定时任务,并唤醒线程
queue.add(task);
if (queue.getMin() == task)
queue.notify();
}
}
为什么需要 queue.getMin() == task 时才调用 notify 方法呢?
因为只有新加入的 task 是所有 Task 中要被最早执行的 task 时,才会需要打断 TimeThread 的等待状态。
2.3.2 取消任务 cancel
// 设置 newTasksMayBeScheduled=false 并清空任务后唤醒线程
public void cancel() {
synchronized(queue) {
thread.newTasksMayBeScheduled = false;
queue.clear();
queue.notify(); // In case queue was already empty.
}
}
该方法会把队列清空,并且把 newTasksMayBeScheduled 标志设置为 false,这个时候如果不调用 queue.notify(),在 queue 本来就已经 empty 的情况下,TimerThread 的 mainloop 就会陷入死等待:
2.3.3 取消任务 cancel
是否上面两种情况调用 notify 就已经足够了?当 queue 为空,并且没人调用 add 或 cancel 方法时,TimerThread 永远都不会 stop,所以机智的 JDK 还加上了一种比较保险的方法:
private final Object threadReaper = new Object() {
protected void finalize() throws Throwable {
synchronized(queue) {
thread.newTasksMayBeScheduled = false;
queue.notify(); // In case queue is empty.
}
}
};
用到了 Object 对象的 finalize 方法,大家都知道 finalize 方法是对象被 GC 的时候调用的。上述做法的思路是:当一个 Timer 已经没有任何对象引用时,自然不会有新的 Task 加入到队列中,Timer 对象自然也就会被垃圾回收,此时 TimerThread 也就应该 stop 了,所以在垃圾回收的时候还应该把 newTasksMayBeScheduled 设置为 false,并且唤起正在 wait 的 TimerThread 线程。所以说,如果你创建的 Timer 不再需要了,最好是调用 cancel 接口手动取消,否则的话 TimerThread 就需要等到垃圾回收的时候才会 stop。
参考:
- 《JDK Timer实现详解》:https://blog.csdn.net/winwill2012/article/details/73939167
每天用心记录一点点。内容也许不重要,但习惯很重要!
JUC源码分析-线程池篇(三)Timer的更多相关文章
- JUC源码分析-线程池篇(三)ScheduledThreadPoolExecutor
JUC源码分析-线程池篇(三)ScheduledThreadPoolExecutor ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor.它主要用来在 ...
- JUC源码分析-线程池篇(一):ThreadPoolExecutor
JUC源码分析-线程池篇(一):ThreadPoolExecutor Java 中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池.在开发过程中,合理地使用线程池 ...
- JUC源码分析-线程池篇(二)FutureTask
JUC源码分析-线程池篇(二)FutureTask JDK5 之后提供了 Callable 和 Future 接口,通过它们就可以在任务执行完毕之后得到任务的执行结果.本文从源代码角度分析下具体的实现 ...
- Elasticsearch源码分析—线程池(十一) ——就是从队列里处理请求
Elasticsearch源码分析—线程池(十一) 转自:https://www.felayman.com/articles/2017/11/10/1510291570687.html 线程池 每个节 ...
- 鸿蒙内核源码分析(线程概念篇) | 是谁在不停的折腾CPU? | 百篇博客分析OpenHarmony源码 | v21.06
百篇博客系列篇.本篇为: v21.xx 鸿蒙内核源码分析(线程概念篇) | 是谁在不断的折腾CPU | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调 ...
- nginx源码分析线程池详解
nginx源码分析线程池详解 一.前言 nginx是采用多进程模型,master和worker之间主要通过pipe管道的方式进行通信,多进程的优势就在于各个进程互不影响.但是经常会有人问道,n ...
- nginx源码分析——线程池
源码: nginx 1.13.0-release 一.前言 nginx是采用多进程模型,master和worker之间主要通过pipe管道的方式进行通信,多进程的优势就在于各个进程互不影 ...
- 鸿蒙内核源码分析(调度机制篇) | 任务是如何被调度执行的 | 百篇博客分析OpenHarmony源码 | v7.07
百篇博客系列篇.本篇为: v07.xx 鸿蒙内核源码分析(调度机制篇) | 任务是如何被调度执行的 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调 ...
- 鸿蒙内核源码分析(时钟任务篇) | 触发调度谁的贡献最大 | 百篇博客分析OpenHarmony源码 | v3.05
百篇博客系列篇.本篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度谁的贡献最大 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度 ...
随机推荐
- iframe调用页面中的局部部分
iframe 调用网页,div遮挡展现局部. <div style=" width:iframe宽度; height:iframe高度; overflow:hidden "& ...
- Win10 打开MSDTC
标签:log com http it sp src c bs io 1,Win+R 打开运行窗口,输入 dcomcnfg,打开组件服务窗口 2,在组件服务 catalo ...
- 8. Jmeter导入jar包
我们都知道Jmeter是Java编写的,所以有很多时候需要用到Java方面的知识.比如Jmeter前置处理器,就用到了很多Java知识.那么本章我们先介绍如何使用Jmeter导人jar包. 工具准备 ...
- 马士兵对话京东T6阿里P7(薪水):月薪5万,他为何要离职?
马士兵大佬你知道吗? 你竟然不知道?你怎么可能不知道!你不知道是不可能的! 记得自己的第一行Java代码,你的Hello World是跟着谁学的吗?我的就是马士兵老师! 马士兵是唯一一个在当时讲课是让 ...
- Mybatis缓存1----系统缓存及简单配置介绍
mybatis缓存 系统缓存:常用的一级缓存和二级缓存 一级缓存 一级缓存是SqlSession级别的缓存,在操作数据库时需要构建SqlSession对象,在对象中有一个数据结构用于存储缓存数据. ...
- Javascript中的相等比较
在比较相等或不相等之前,会对操作数进行类型转换,然后比较相等性 在转换不同的数据类型时,相等和不相等操作符遵循下列基本规则: 1.如果由一个操作数是布尔值,则在比较相等性之前先将其转换为数值:2.如果 ...
- Zookeeper 集群的安装及高可用性验证已完成!
安装包 kafka_2.12-0.10.2.0.tgz zookeeper-3.3.5.tar.gz Java 环境 Zookeeper 和 Kafka 的运行都需要 Java 环境,Kafka 默认 ...
- linux终端命令行缩短显示路径
1,修改.bashrc文件(用户根目录下) vim 打开.bashrc文件,找到如下这行: else PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ ...
- [机器学习][face recognition] 一个视频人脸识别实现
开发环境和用到的库: Ubuntu jupyter notebook(python3.6) OpenCV Dlib face_recognition 实现效果如下(视频截图): #coding=utf ...
- 2018-12-2-C#-Span-入门
title author date CreateTime categories C# Span 入门 lindexi 2018-12-02 11:32:46 +0800 2018-06-18 11:1 ...