JDK定时器Timer原理

news2024/9/20 14:23:26

前言

前些时间想到利用redis实现延时队列,但是底层的定时器不止如何实现好些,故此研究了一下jdk的Timer。

Timer是一个用于执行定时任务的类,可以单次执行或按指定时间间隔循环执行(直到主动cancel或线程被杀掉)。Timer中任务处理采用了生产者-消费者模型的设计思想。

原理简述

Timer里面维护了一个TimerThread(继承了Thread)和TaskQueue(其实就是一个初始化长度为128的TimerTask数组【TimerTask就是我们的执行任务】),当我们new Timer()的时候就会调用TimerThread.start方法,然后TimerThread线程while(true)循环从TaskQueue中取出任务(TimerTask)执行,那么如何取出执行时间最近的任务呢?前面有提到TaskQueue底层是一个数组,但其实是个小根堆,只要每次取出根元素就是执行时间最近的一个任务了。

ps:TimerTask里面有一个long类型的nextExecutionTime(下次执行时间)变量,小根堆就是根据这个来排序的…
在这里取出最近的任务并不是取的小根堆的根元素,而是TaskQueue中下标为1的元素,也就是第二个元素

组件介绍

Timer关键属性:TaskQueue、TimerThread

TaskQueue:任务队列

一个存储任务的任务池TaskQueue,包含一个初始大小为128的TimerTask数组,负责任务的存储(add)、排序(fixUp、fixDown)、取出(getMin)、清理(removeMin、quickRemove)、循环任务处理(rescheduleMin)以及一些其他基本操作。并通过排序保证队头任务的执行一定是最早的。
根据注释可以知道TimerTask是一个用做一个平衡二叉树的模型,一个父节点array[n]下挂载的两个子节点为array[2n]和array[2n+1].
在这里插入图片描述

TimerTask:任务实体

TimerTask是任务实体,Runnable接口的实现类。内部包含用于线程安全的锁lock、用于标记任务状态的字段state、以及一个供用户实现的任务内容抽象方法run().

public abstract class TimerTask implements Runnable {
    /** 此对象用于控制对TimerTask内部的访问。 */
    final Object lock = new Object();

    /** 标记任务状态的字段 初始化为0 */
    int state = VIRGIN;

    /** 该状态表示任务尚未执行。在TimerTask对象创建时,它的状态就是VIRGIN。*/
    static final int VIRGIN = 0;
    /** 
     * 表示任务已经被调度,等待执行。当调用Timer.schedule()方法成功后,任务的状态将变为SCHEDULED。
     * 此时任务已经被加入了任务队列中,等待Timer线程按照任务的调度时间来执行。
     */
    static final int SCHEDULED   = 1;
    /** 表示任务已经执行完成。此时任务的run()方法已经执行完毕,但任务对象还没有从任务队列中删除,因为队列中的任务删除是由Timer线程自动完成的。*/
    static final int EXECUTED    = 2;
    /** 
     * 该状态表示任务已经被取消。当调用TimerTask.cancel()方法取消任务时,任务的状态将变为CANCELLED。
     * 此时任务被标记为取消状态,即使它已经被加入了任务队列中,也不会执行。一旦任务被标记为CANCELLED状态,它将永远不会被执行。
     */
    static final int CANCELLED   = 3;

    /**
     * 此任务的下一次执行时间,格式为System.currentTimeMillis,假设此任务计划执行。对于重复任务,此字段在每次任务执行之前更新。
     */
    long nextExecutionTime;
    /**
     * 重复任务的周期(毫秒)。正值表示固定利率执行。负值表示固定延迟执行。值0表示非重复任务。
     */
    long period = 0;
    // ...

TimerThread:事件消费者

一个作为事件消费者的TimerThread,TimerThread中不断获取当前任务队列的队头任务,执行任务。并根据任务是否需要循环决定是移除任务还是将任务按下一次执行时间重新加入到任务队列中。在TimerThread中不断获取待执行任务时,采用了Object.wait()和Object.notify()的机制。Object.wait()保证了任务队列为空时及时释放资源,而当有新的任务时也通过Object.notify()及时恢复任务的遍历。
在这里插入图片描述
Timer本身提供了对以上三者操作的封装、实例化和对外暴露运行任务的接口。同时,作为生产者,将用户任务加入到任务队列;对消费者层面,Timer也是和消费者线程唯一绑定的,负责启动消费者线程,并在生产了新的任务后及时通知已经休眠的消费者。提供了多种构造方法和清理接口。

源码解析

Timer的使用demo

public class TimerDemo {

    public static void main(String[] args) {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                String strDate = format.format(new Date());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) { }
                System.err.println("strDate1 = " + strDate);
            }
        }, 5, 5000);
    }

}

Timer构造方法:

// Timer.java  131行
public Timer() {
    this("Timer-" + serialNumber());
}
// Timer.java  158行
public Timer(String name) {
    // 设置线程名称
    thread.setName(name);
    // 启动TimerThread线程
    thread.start();
}

TimerThread继承了Thread类,所以我们看一下它的run方法:

// Timer.java   503行
public void run() {
    try {
        mainLoop();
    } finally {
        // Someone killed this Thread, behave as if Timer cancelled
        // 【有人杀死了这个线程,表现得就像计时器被取消一样】
        synchronized(queue) {
            newTasksMayBeScheduled = false;
            queue.clear();  // Eliminate obsolete references【消除过时的引用】
        }
    }
}

跟进去mainLoop()方法

/**
 * 主计时器循环
 */
 // Timer.java   518行
private void mainLoop() {
    while (true) {
        try {
            TimerTask task;
            boolean taskFired;
            synchronized(queue) {
                // 线程为空则wait
                while (queue.isEmpty() && newTasksMayBeScheduled)
                    queue.wait();
                if (queue.isEmpty())
                    break; // 队列为空,将永远保留;死亡
                    
                long currentTime, executionTime;
                // 获取距离执行时间最近的任务(返回的是queue[1]的引用)
                task = queue.getMin();
                synchronized(task.lock) {
                    // 如果任务的状态是已经取消,则移除该任务,移除后TimerTask数组元素将重新排序
                    if (task.state == TimerTask.CANCELLED) {
                        queue.removeMin();
                        continue;  // No action required, poll queue again
                    }
                    currentTime = System.currentTimeMillis();
                    // 该任务的下次执行时间
                    executionTime = task.nextExecutionTime;
                    // 若任务的下次执行时间 < 当前时间
                    if (taskFired = (executionTime<=currentTime)) {
                        // period值0表示非重复任务。将该任务从任务队列移除,并且将任务的state设置为已执行
                        if (task.period == 0) { // Non-repeating, remove
                            queue.removeMin();
                            task.state = TimerTask.EXECUTED;
                        } else { // Repeating task, reschedule
                            // 将与头任务关联的nextExecutionTime设置为指定值,并相应地调整优先级队列。
                            queue.rescheduleMin(
                              task.period<0 ? currentTime   - task.period
                                            : executionTime + task.period);
                        }
                    }
                }
                // 还未到执行时间,wait
                if (!taskFired)
                    queue.wait(executionTime - currentTime);
            }
            // 执行时间到了,执行任务的run方法【这里就是我们自己写的逻辑了】
            if (taskFired)
                task.run();
        } catch(InterruptedException e) {
        }
    }
}

如上,若queue为空&&newTasksMayBeScheduled为true,则调用wait方法等待,若queue不为空则调用queue.getMin获取距离执行时间最近的任务(返回的是queue[1]的引用),然后判断任务的state和nextExecutionTime(下次执行时间),条件满足则执行任务;如果任务的state是已经取消,则移除该任务,移除后TimerTask数组元素将重新排序,若还未到执行时间则wait。

ps:为什么queue.getMin是返回queue[1]元素?
答:TaskQueue内部使用一个数组queue来存储所有的TimerTask对象。数组的第一个元素queue[0]没有使用,实际的任务从数组的第二个元素queue[1]开始存储。数组中的元素是按照任务的执行时间从小到大排序的,也就是说,queue[1]元素表示最先执行的任务。

接下来再看Timer.schedule()方法:

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.");
    // 进去看看,参数1:任务 参数二:下次执行时间 参数三:重复任务的周期(毫秒)
    sched(task, System.currentTimeMillis()+delay, -period);
}

// Timer.java   386行
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);
        // 若当前添加的任务是距离当前执行时间最近的任务则唤醒等待线程【其实就是TimerThread线程】
        if (queue.getMin() == task)
            queue.notify();
    }
}

最后有几点需要注意:
1)在不使用时一定要及时cancel清理,释放资源。
2)当timer中有多任务时,因为后边任务会依赖前边任务执行完,尤其是如果有耗时任务,会发生定时不准确的现象。
3)当存在多任务时,若其中某个因异常而终止,则会退出所有任务的执行(消费者线程被异常终止了)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/358210.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大数据之---Nifi-Nifi模板_具体使用方法---大数据之Nifi工作笔记0009

然后我们来看看,如果好不容易设计了一个流程,那么是可以通过 使用模板来让流程复用的 可以看到可以创建模板,一会说怎么用具体,上面已经 写清楚了,如何创建模板 我们看一下左侧的operate,这里可以看到这里就可以创建模板 可以看到小手那个地方,点击就可以创建模板了 可以看到…

python k8s库,read_namespaced_config_map:maximum recursion depth exceeded

使用背景 在python中&#xff0c;调用了gevent库&#xff0c;同时引用了官方的k8s库接口&#xff1a; GitHub - kubernetes-client/python: Official Python client library for kubernetesOfficial Python client library for kubernetes. Contribute to kubernetes-client/…

Node.js安装配置及Angular CLI的安装

NodeJS的安装node.js官网下载地址: https://nodejs.org/en/download/在node.js的官网上面下载适合自己机型的&#xff0c;如果是Windows系统的话&#xff0c;建议下载对应的 Windows Installer (.msi) 。下载完成后&#xff0c;双击打开安装&#xff0c;安装路径最好自定义&…

Melis4.0[D1s]:1.启动流程(与adc按键初始化相关部分)跟踪笔记

文章目录1.启动流程1.1 最先进入的文件&#xff1a;head_s.S1.2 start_kernel()函数所在的文件&#xff1a;init.c1.3 input_init()函数所在文件&#xff1a;sys_input.c1.4 INPUT_LKeyDevInit()所在文件&#xff1a;keyboarddev.c1.5 esINPUT_RegLdev()所在文件&#xff1a;in…

LVS负载均衡

文章目录前言一、LVS模式-DR二、ipvsadm配置参数三、DR模式的部署server1:调度器&#xff08;VS&#xff09;server2:真实服务器&#xff08;RS&#xff09;server3:真实服务器&#xff08;RS&#xff09;真实服务器(server2和server3)屏蔽客户端测试&#xff1a;纯代码步骤演示…

SpringCloud保姆级搭建教程六---ElasticSearch

es下载地址&#xff1a;https://www.elastic.co/cn/downloads/elasticsearch 最新版本或者 https://github.com/elastic/elasticsearch 7.17.9kibana下载地址&#xff1a;https://github.com/elastic/kibana 各个版本jdk8 对应的es应该是7.*版本&#xff0c;最新的es应该对应的…

【论文及代码详解】BEIT: BERT Pre-Training of Image Transformers

记录下论文《BEIT: BERT Pre-Training of Image Transformers》&#xff0c;这是一篇将Transformer应用于图像领域&#xff0c;并使用自监督方法进行参数初始化的文章。 论文链接 整体概要 由于网络整体流程图没有标注好模型的运行过程&#xff0c;结合论文的描述&#xff1a…

收藏,核心期刊的投稿、审稿、出刊流程详解

学术期刊论文&#xff08;核心和普刊&#xff09;的发表流程总的来说其实是一样的&#xff0c;整个流程包括&#xff1a;1写作-2选择刊物-3投稿-4审稿-5返修或拒稿-6录用-7出刊-8上网检索。 其中1和2其实顺序是可以调换的&#xff0c;可以选择好刊物再写作&#xff0c;根据刊物…

麦克风阵列波束基本概念理解

波束形成 本质上是设计合适的滤波器&#xff0c;对于一类固定滤波器系数的阵列来说&#xff0c;无论输入信号或者噪声信号的统计特征如何&#xff0c;其滤波器系数固定不变&#xff0c;此类波束形成叫Fixed Beamforming&#xff0c;固定波束形成好比传统数字信号处理里面的经典…

TCP并发服务器(多进程与多线程)

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起探讨和分享Linux C/C/Python/Shell编程、机器人技术、机器学习、机器视觉、嵌入式AI相关领域的知识和技术。 TCP并发服务器&#xff08;多进程与多线程&#xff09;1. 多进程并发服务器&#xff08;1&#xff09;…

NPDP认证|2023年,0基础转行产品经理可以吗?

2023年&#xff0c;告别了疫情&#xff0c;各个行业正在快速回暖&#xff0c;很多企业都在高薪招聘产品经理岗位&#xff0c;这让很多其他岗位的朋友也想转行做产品经理&#xff0c;那没有基础&#xff0c;没有经验能转行做产品经理吗&#xff1f; 0基础转行产品经理是可能的&a…

Redis 删除策略

过期数据Redis中的数据特征 Redis是一种内存级数据库&#xff0c;所有数据均存放在内存中&#xff0c;内存中的数据可以通过TTL指令获取其状态XX &#xff1a;具有时效性的数据-1 &#xff1a;永久有效的数据-2 &#xff1a;已经过期的数据 或 被删除的数据 或 未定义的数据数…

Windows出现0xc00d36e5错误怎么办?

当我们使用Windows Media Player来播放视频文件时&#xff0c;可能会出现无法播放&#xff0c;并显示0xc00d36e5错误代码。该错误可能是因为Windows Media Player不支持视频格式、注册表项损坏、系统配置问题、第三方应用程序冲突等。下面将开始介绍0xc00d36e5错误的解决方法&a…

K_A12_014 基于STM32等单片机驱动S12SD紫外线传感器模块 串口与OLED0.96双显示

K_A12_014 基于STM32等单片机驱动S12SD紫外线传感器模块 串口与OLED0.96双显示一、资源说明二、基本参数参数引脚说明三、驱动说明IIC地址/采集通道选择/时序对应程序:数据对比&#xff1a;四、部分代码说明1、接线引脚定义1.1、STC89C52RCS12SD紫外线传感器模块1.2、STM32F103…

【数据结构】P0 三要素与五个特征

三要素与五个特征什么是数据结构数据结构的三要素逻辑结构存储结构数据的运算算法的五个特征时间复杂度什么是数据结构 数据元素之间存在着一种或者多种关系&#xff0c;这种关系称为结构。 数据结构的三要素 数据结构的三要素&#xff1a;逻辑结构、存储结构、数据的运算。 …

【面试题】手写防抖和节流

1. 手写防抖 debounce 首先介绍一个防抖的应用场景。假如需要监听一个输入框在输入文字后触发的change事件&#xff0c;那么通过keyup事件&#xff0c;每次输入文字后都会触发change事件&#xff0c;频繁触发的情况会影响系统的性能。因此可以使用防抖来降低触发频率&#xff…

flutter系列之:在flutter中使用导航Navigator

文章目录简介flutter中的NavigatorNavigator的使用总结简介 一个APP如果没有页面跳转那么是没有灵魂的&#xff0c;页面跳转的一个常用说法就是Navigator,flutter作为一个最为优秀的前端框架&#xff0c;Navigator肯定是必不可少的&#xff0c;那么在flutter中如何使用Navigat…

自建Git服务器

Gitea - Git with a cup of tea是一个国外团队基于国内一位大牛写的gogs开源项目&#xff08;Go语言开发&#xff09;二次开发的轻量Git社区&#xff0c;其稳定性非常好&#xff0c;而且是非常轻量级在个人亲测在1核1G的centos7主机上1个月不重启依然稳定运行&#xff0c;引用g…

chatgpt怎么去玩?解析各种用途和强大的功能

关于chatgpt怎么玩&#xff1f;他的一些原理以及玩法&#xff0c;相信大家都是挺好奇的吧&#xff0c;毕竟这个新的人工智能和以往我们玩过的&#xff0c;是完全不一样的&#xff0c;它具备更多的可玩性&#xff0c;而且具备有一定的学习能力&#xff0c;这就造就了它的强大&am…

记一次IDE的Docker插件实战(Dockfile篇)

IDEA下使用Docker插件制作镜像、推送及运行 前言 本部分主要根据IDEA的Docker插件实战(Dockerfile篇)_程序员欣宸的博客-CSDN博客_idea编写dockerfile一文所述内容进行实践&#xff0c;并对其中遇到的问题进行解答&#xff0c;从而串接多个知识点。 如何编写Dockfile 在Int…