当前位置: 首页 > news >正文

【Java】定时任务 - Timer/TimerTask 源码原理解析

一、背景及使用

日常实现各种服务端系统时,我们一定会有一些定时任务的需求。比如会议提前半小时自动提醒,异步任务定时/周期执行等。那么如何去实现这样的一个定时任务系统呢? Java JDK提供的Timer类就是一个很好的工具,通过简单的API调用,我们就可以实现定时任务。

现在就来看一下java.util.Timer是如何实现这样的定时功能的。

首先,我们来看一下一个使用demo

Timer timer = new Timer(); 
TimerTask task = new TimerTask() { public void run() { System.out.println("executing now!"); } 
};// 延迟 1s 打印一次 
timer.schedule(task, 1000) 
// 延迟 1s 固定时延每隔 1s 周期打印一次
timer.schedule(task, 1000, 1000); 
// 延迟 1s 固定速率每隔 1s 周期打印一次
timer.scheduleAtFixRate(task, 1000, 1000)

基本的使用方法:

  1. 创建一个Timer对象

  2. 创建一个TimerTask对象,在这里实现run方法

  3. 将TimerTask对象作为参数,传入到Timer对象的scheule方法中,进行调度执行。

加入任务的API如下:

  • 一次性任务的API

   // 指定时延后运行// 默认fixed-delay模式,周期时间按上一次执行结束时间计算public void schedule(TimerTask task, long delay) {if (delay < 0)throw new IllegalArgumentException("Negative delay.");sched(task, System.currentTimeMillis()+delay, 0);}// 指定时间点运行public void schedule(TimerTask task, Date time) {sched(task, time.getTime(), 0);}
  • 周期任务的APi:

    // 指定时延后运行,之后以指定周期运行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);}// 指定时间点运行,之后以指定周期运行public void schedule(TimerTask task, Date firstTime, long period) {if (period <= 0)throw new IllegalArgumentException("Non-positive period.");sched(task, firstTime.getTime(), -period);}// 指定时延后运行,之后以指定周期运行// 默认fixedRate模式,周期时间按任务执行开始时间计算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);}public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period) {if (period <= 0)throw new IllegalArgumentException("Non-positive period.");sched(task, firstTime.getTime(), period);}

可以看到API方法内部都是调用sched方法,其中time参数下一次任务执行时间点,是通过计算得到。period参数为0的话则表示为一次性任务。

二、实现原理

那么我们来看一下Timer内部是如何实现调度的。

2.1、内部结构

先看一下Timer的组成部分:

public class Timer {// 任务队列private final TaskQueue queue = new TaskQueue();// 工作线程,循环取任务private final TimerThread thread = new TimerThread(queue);private final Object threadReaper = new Object() {protected void finalize() throws Throwable {synchronized(queue) {thread.newTasksMayBeScheduled = false;queue.notify(); // In case queue is empty.}}};// Timer的序列号,命名工作线程(静态变量,在启动多个Timer的情况可以用于区分对应的工作线程)private final static AtomicInteger nextSerialNumber = new AtomicInteger(0);  }

Timer有3个重要的模块,分别是 TimerTask, TaskQueue, TimerThread

  • TimerTask,即待执行的任务

  • TaskQueue,任务队列,TimerTask加入后会按执行时间自动排序

  • TimerThread,工作线程,真正循环执行TimerTask的线程

那么,在加入任务之后,整个Timer是怎么样运行的呢?可以看下面的示意图:

2.2、工作线程

  • 创建任务,调用scheule方法
public void schedule(TimerTask task, Date firstTime, long period) { if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, firstTime.getTime(), -period); 
}
  • 内部调用sched方法

// sched方法的入参是task任务,执行的时间,以及执行周期
private void sched(TimerTask task, long time, long period) {if (time < 0)throw new IllegalArgumentException("Illegal execution time.");// 防止溢出if (Math.abs(period) > (Long.MAX_VALUE >> 1))period >>= 1;// 对queue加锁,避免并发入队synchronized(queue) {if (!thread.newTasksMayBeScheduled)throw new IllegalStateException("Timer already cancelled.");// 对task加锁,避免并发修改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);/* 如果任务是队列当前第一个任务,则唤醒工作线程这里是因为工作线程处理完上一个任务之后,会 sleep 到下一个 task 的执行时间点。如果有 nextExecutionTime 更早的 task 插队到前面,则需要马上唤醒工作线程进行检查避免 task 延迟执行*/if (queue.getMin() == task)queue.notify();}
}

流程中加了一些锁,用来避免同时加入TimerTask的并发问题。可以看到sched方法的逻辑比较简单,task赋值之后入队,队列会自动按照nextExecutionTime排序(升序,排序的实现原理后面会提到)。

  • 工作线程的mainLoop
public void run() {try {mainLoop();} finally {synchronized(queue) {newTasksMayBeScheduled = false;queue.clear();}}
}/*** 工作线程主逻辑,循环执行*/
private void mainLoop() {while (true) {try {TimerTask task;boolean taskFired; // 标记任务是否应该执行synchronized(queue) {// 如果队列为空,且newTasksMayBeScheduled为true,此时等待任务加入while (queue.isEmpty() && newTasksMayBeScheduled)queue.wait();// 如果队列为空,且newTasksMayBeScheduled为false,说明此时线程应该退出if (queue.isEmpty())break;// 队列不为空,尝试从队列中取task(目标执行时间最早的task)long currentTime, executionTime;task = queue.getMin();synchronized(task.lock) {// 校验task状态if (task.state == TimerTask.CANCELLED) {queue.removeMin();continue;}currentTime = System.currentTimeMillis();executionTime = task.nextExecutionTime;// 当前时间 >= 目标执行时间,说明任务可执行,设置taskFired = trueif (taskFired = (executionTime<=currentTime)) {if (task.period == 0) { // period == 0 说明是非周期任务,先从队列移除queue.removeMin();task.state = TimerTask.EXECUTED;} else { // 周期任务,会根据period重设执行时间,再加入到队列中queue.rescheduleMin(task.period<0 ? currentTime   - task.period: executionTime + task.period);}}}if (!taskFired) // 任务为不需执行状态,则等待queue.wait(executionTime - currentTime);}if (taskFired)  // 任务需要执行,则调用task的run方法执行,这里执行的其实就是调用方创建task时候run方法的逻辑task.run();} catch(InterruptedException e) {}}
}

mainLoop的源码中可以看出,基本的流程如下所示

当发现是周期任务时,会计算下一次任务执行的时间,这个时候有两种计算方式,即前面API中的

  • schedule:period为负值,下次执行时间

  • scheduleAtFixedRate:period为正值

queue.rescheduleMin( task.period<0 ? currentTime - task.period : executionTime + task.period);

2.3、优先队列

当从队列中移除任务,或者是修改任务执行时间之后,队列会自动排序。始终保持执行时间最早的任务在队首。 那么这是如何实现的呢?

看一下TaskQueue的源码就清楚了

class TaskQueue {private TimerTask[] queue = new TimerTask[128];private int size = 0;int size() {return size;}void add(TimerTask task) {if (size + 1 == queue.length)queue = Arrays.copyOf(queue, 2*queue.length);queue[++size] = task;fixUp(size);}TimerTask getMin() {return queue[1];}TimerTask get(int i) {return queue[i];}void removeMin() {queue[1] = queue[size];queue[size--] = null;  // Drop extra reference to prevent memory leakfixDown(1);}void quickRemove(int i) {assert i <= size;queue[i] = queue[size];queue[size--] = null;  // Drop extra ref to prevent memory leak}void rescheduleMin(long newTime) {queue[1].nextExecutionTime = newTime;fixDown(1);}boolean isEmpty() {return size==0;}void clear() {for (int i=1; i<=size; i++)queue[i] = null;size = 0;}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 kidif (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);}
}

可以看到其实TaskQueue内部就是基于数组实现了一个最小堆 (balanced binary heap), 堆中元素根据 执行时间nextExecutionTime排序,执行时间最早的任务始终会排在堆顶。这样工作线程每次检查的任务就是当前最早需要执行的任务。堆的初始大小为128,有简单的倍增扩容机制。

2.4、其他方法

TimerTask 任务有四种状态:

  • VIRGIN: 任务刚刚创建,还没有schedule

  • SCHEDULED:任务已经schedule,进入到队列中

  • EXECUTED: 任务已执行/执行中

  • CANCELLED:任务已取消

Timer 还提供了cancelpurge方法

  • cancel,清除队列中所有任务,工作线程退出。

  • purge,清除队列中所有状态置为取消的任务。

2.5、常见应用实现

Java的Timer广泛被用于实现异步任务系统,在一些开源项目中也很常见, 例如消息队列RocketMQ的 延时消息/消费重试 中的异步逻辑。

public void start() {if (started.compareAndSet(false, true)) {super.load();// 新建了一个timerthis.timer = new Timer("ScheduleMessageTimerThread", true);for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) {// ...}// 调用了Timer的scheduleAtFixedRate方法this.timer.scheduleAtFixedRate(new TimerTask() {@Overridepublic void run() {try {if (started.get()) {ScheduleMessageService.this.persist();}} catch (Throwable e) {log.error("scheduleAtFixedRate flush exception", e);}}}, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval());}
}

上面这段代码是RocketMQ的延时消息投递任务 ScheduleMessageService 的核心逻辑,就是使用了Timer实现的异步定时任务。

三、总结

不管是实现简单的异步逻辑,还是构建复杂的任务系统,Java的Timer确实是一个方便实用,而且又稳定的工具类。从Timer的实现原理,我们也可以窥见定时系统的一个基础实现:线程循环 + 优先队列。这对于我们自己去设计相关的系统,也会有一定的启发。

设计亮点个人总结:

  1. 流程中加了一些锁,用来避免TimeTask同时加入TimerTaskQueue、对TimeTsk修改所带来的并发问题

  2. 通过newTasksMayBeScheduled 来控制工作线程对TimerTaskQueue的执行

  3. 采用小根堆算法来对TimerTaskQueue进行优先级排序

  4. 工作线程通过循环来执行TimerTaskQueue的任务,还判断并处理了Task的状态机及流转

相关文章:

【Java】定时任务 - Timer/TimerTask 源码原理解析

一、背景及使用 日常实现各种服务端系统时&#xff0c;我们一定会有一些定时任务的需求。比如会议提前半小时自动提醒&#xff0c;异步任务定时/周期执行等。那么如何去实现这样的一个定时任务系统呢&#xff1f; Java JDK提供的Timer类就是一个很好的工具&#xff0c;通过简单…...

SAP ABAP基础语法-Excel上传(十)

EXCEL BDS模板上传及赋值 上传模板事务代码&#xff1a;OAER l 功能代码&#xff1a;向EXCEL模板中写入数据示例代码如下 REPORT ZEXCEL_DOI. “doi type pools TYPE-POOLS: soi. *SAP Desktop Office Integration Interfaces DATA: container TYPE REF TO cl_gui_custom_c…...

记录一次某某虚拟机的逆向

导语 学了一段时间的XPosed&#xff0c;发现XPosed真的好强&#xff0c;只要技术强&#xff0c;什么操作都能实现... 这次主要记录一下我对这款应用的逆向思路 apk检查 使用MT管理器检查apk的加壳情况 发现是某数字的免费版本 直接使用frida-dexdump 脱下来后备用 应用分…...

upload-labs关卡7(基于黑名单的空格绕过)通关思路

文章目录 前言一、回顾上一关知识点二、靶场第七关通关思路1、看源代码2、空格绕过3、检查文件是否成功上传 总结 前言 此文章只用于学习和反思巩固文件上传漏洞知识&#xff0c;禁止用于做非法攻击。注意靶场是可以练习的平台&#xff0c;不能随意去尚未授权的网站做渗透测试…...

CnosDB 在最近新发布的 2.4.0 版本中增加对时空函数的支持。

CnosDB 在最近新发布的 2.4.0 版本中增加对时空函数的支持。 概述 时空函数是一种用于描述时空结构和演化的函数。它在物理学、数学和计算机科学等领域中都有广泛的应用。时空函数可以描述物体在时空中的位置、速度、加速度以及其他相关属性。 用法 CnosDB 将使用一种全新的…...

python实现炒股自动化,个人账户无门槛量化交易的开始

本篇作为系列教程的引子&#xff0c;对股票量化程序化自动交易感兴趣的朋友可以关注我&#xff0c;现在只是个粗略计划&#xff0c;后续会根据需要重新调整&#xff0c;并陆续添加内容。 股票量化程序化自动交易接口 很多人在找股票个人账户实现程序化自动交易的接口&#xff0…...

推荐系统笔记--Swing模型的原理

1--Swing模型的引入 在 Item CF 召回中&#xff0c;物品的相似度是基于其受众的交集来衡量的&#xff0c;但当受众的交集局限在一个小圈子时&#xff0c;就会误将两个不相似的物品定义为相似&#xff1b; Swing 模型引入用户的重合度来判断两个用户是否属于一个小圈子&#xff…...

联想小新Pro14默认设置的问题

联想小新Pro14 锐龙版&#xff0c;Win11真的挺多不习惯的&#xff0c;默认配置都不符合一般使用习惯。 1、默认人走过自动开机。人机互动太强了&#xff1b; 2、默认短超时息屏但不锁屏&#xff0c;这体验很容易觉得卡机然后唤起&#xff0c;却又不用密码打开&#xff1b; 3…...

【洛谷 P5019】[NOIP2018 提高组] 铺设道路 题解(分治算法+双指针)

[NOIP2018 提高组] 铺设道路 题目背景 NOIP2018 提高组 D1T1 题目描述 春春是一名道路工程师&#xff0c;负责铺设一条长度为 n n n 的道路。 铺设道路的主要工作是填平下陷的地表。整段道路可以看作是 n n n 块首尾相连的区域&#xff0c;一开始&#xff0c;第 i i i …...

牛客刷题记录11.12

继承和组合 二进制数统计 1的个数 和 0 的个数...

NextJS开发:使用IconPark、Lucide图标库

IconPark、Lucide两个很不错的图标库&#xff0c;如果需要用到微信、阿里等国内logo可以使用IconPark&#xff0c;Lucide中没有包含这些内容。 安装IconPark npm install icon-park/react --save简单使用 import {Home} from icon-park/react;<Home/> <Home theme&…...

11.12总结

这一周主要写了个人中心的几个功能&#xff0c;资料修改&#xff0c;收货地址的创建和修改删除&#xff0c;还有主页界面和商品界面...

Gogs安装和部署教程-centos上

0、什么是 Gogs? Gogs 是一款极易搭建的自助 Git 服务。 Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自助 Git 服务。使用 Go 语言开发使得 Gogs 能够通过独立的二进制分发&#xff0c;并且支持 Go 语言支持的 所有平台&#xff0c;包括 Linux、Mac OS X、Windo…...

Unity中Shader雾效的实现方法一

文章目录 前言一、在片元着色器中使用如下公式计算最终的颜色 lerp(雾效颜色&#xff0c;物体颜色&#xff0c;雾效混合因子)1、获取雾效颜色2、物体的颜色一般通过纹理采样得到&#xff0c;此处用 1 代替测试3、获取 雾效混合因子&#xff08;由 雾的距离 和 雾的浓度决定&am…...

Mac安装配置Tomcat,以及使用(详解)

目录 一、Tomcat下载&#xff1a; 1、左栏选择Tomcat版本 2、点击下载即可&#xff0c;任选其一 ​编辑3、下载好的文件夹放到用户名下即可&#xff08;之前已经下载过&#xff0c;这里以Tomcat 8.5.88为演示&#xff09;&#xff0c;这里提供8.5.88的安装包&#xff1a; 二…...

Smart Link 和 Monitor Link应用

定义 Smart Link常用于双上行链路组网&#xff0c;提高接入的可靠性。 Monitor Link通过监视上行接口&#xff0c;使下行接口同步上行接口状态&#xff0c;起到传递故障信息的作用。 Smart Link&#xff0c;又叫做备份链路。一个Smart Link由两个接口组成&#xff0c;其中一个…...

【debug】解决Kali虚拟机开机黑屏,左上角光标一直闪动无法开机问题

做网络攻防实验时&#xff0c;突然Kali无法打开&#xff0c;遇到这个问题。。。。。。 遇到的问题 突然kali虚拟机变成如下黑屏&#xff0c;无法开机&#xff0c;左上角光标闪动&#xff0c;重启无效。 解决办法 在上图界面&#xff0c;按Ctrl F3&#xff08;不同电脑快捷键…...

目标检测YOLO实战应用案例100讲-基于改进YOLO算法的道路交通目标检测(续)

目录 3.3 实验结果与分析 3.3.1 实验数据集 3.3.2 算法的评价指标 3.3.3 损失函数实验结果...

爬虫怎么伪装才更安全

随着网络技术的不断发展&#xff0c;爬虫技术也越来越成熟&#xff0c;爬虫伪装技术也随之得到了广泛应用。在爬虫伪装技术中&#xff0c;如何伪装成正常的浏览器行为&#xff0c;让目标网站无法辨别出爬虫的存在&#xff0c;是爬虫伪装技术的核心。下面&#xff0c;我将从以下…...

openssl+sha256开发实例(C++)

文章目录 一、 sha256介绍二、sha256原理三、openssl sha256实现 一、 sha256介绍 SHA-256&#xff08;Secure Hash Algorithm 256-bit&#xff09;是一种哈希算法&#xff0c;属于 SHA-2&#xff08;Secure Hash Algorithm 2&#xff09;家族的一员。SHA-256 产生的哈希值是一…...

基于算法竞赛的c++编程(28)结构体的进阶应用

结构体的嵌套与复杂数据组织 在C中&#xff0c;结构体可以嵌套使用&#xff0c;形成更复杂的数据结构。例如&#xff0c;可以通过嵌套结构体描述多层级数据关系&#xff1a; struct Address {string city;string street;int zipCode; };struct Employee {string name;int id;…...

Leetcode 3576. Transform Array to All Equal Elements

Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接&#xff1a;3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到&#xf…...

C++:std::is_convertible

C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...

华为OD机试-食堂供餐-二分法

import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...

Spring AI 入门:Java 开发者的生成式 AI 实践之路

一、Spring AI 简介 在人工智能技术快速迭代的今天&#xff0c;Spring AI 作为 Spring 生态系统的新生力量&#xff0c;正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务&#xff08;如 OpenAI、Anthropic&#xff09;的无缝对接&…...

如何理解 IP 数据报中的 TTL?

目录 前言理解 前言 面试灵魂一问&#xff1a;说说对 IP 数据报中 TTL 的理解&#xff1f;我们都知道&#xff0c;IP 数据报由首部和数据两部分组成&#xff0c;首部又分为两部分&#xff1a;固定部分和可变部分&#xff0c;共占 20 字节&#xff0c;而即将讨论的 TTL 就位于首…...

Rapidio门铃消息FIFO溢出机制

关于RapidIO门铃消息FIFO的溢出机制及其与中断抖动的关系&#xff0c;以下是深入解析&#xff1a; 门铃FIFO溢出的本质 在RapidIO系统中&#xff0c;门铃消息FIFO是硬件控制器内部的缓冲区&#xff0c;用于临时存储接收到的门铃消息&#xff08;Doorbell Message&#xff09;。…...

九天毕昇深度学习平台 | 如何安装库?

pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子&#xff1a; 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...

保姆级教程:在无网络无显卡的Windows电脑的vscode本地部署deepseek

文章目录 1 前言2 部署流程2.1 准备工作2.2 Ollama2.2.1 使用有网络的电脑下载Ollama2.2.2 安装Ollama&#xff08;有网络的电脑&#xff09;2.2.3 安装Ollama&#xff08;无网络的电脑&#xff09;2.2.4 安装验证2.2.5 修改大模型安装位置2.2.6 下载Deepseek模型 2.3 将deepse…...

JS手写代码篇----使用Promise封装AJAX请求

15、使用Promise封装AJAX请求 promise就有reject和resolve了&#xff0c;就不必写成功和失败的回调函数了 const BASEURL ./手写ajax/test.jsonfunction promiseAjax() {return new Promise((resolve, reject) > {const xhr new XMLHttpRequest();xhr.open("get&quo…...