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

news2024/11/15 16:49:36

一、背景及使用

日常实现各种服务端系统时,我们一定会有一些定时任务的需求。比如会议提前半小时自动提醒,异步任务定时/周期执行等。那么如何去实现这样的一个定时任务系统呢? 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 = true
                    if (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 leak
        fixDown(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 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);
    }
}

可以看到其实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();
        // 新建了一个timer
        this.timer = new Timer("ScheduleMessageTimerThread", true);
        for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) {
            // ...
        }
        
        // 调用了Timer的scheduleAtFixedRate方法
        this.timer.scheduleAtFixedRate(new TimerTask() {

            @Override
            public 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的状态机及流转

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

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

相关文章

【Qt】撤销/恢复的快捷键

使用Qt的时候&#xff0c;有时需要撤销修改的代码&#xff0c;但可能回撤过头了。 下面提供2个快捷键&#xff0c;当撤销过头时&#xff0c;可恢复撤销内容。 撤销的快捷键是 CtrlZ 恢复/向前的快捷键是 CtrlShiftZ 我们可以自定义快捷键。 点击【工具】->【选项】 点击…

基于XML的声明式事务

场景模拟 参考基于注解的声明式事务 修改Spring的配置文件 将Spring配置文件中去掉tx:annotation-driven标签&#xff0c;并添加配置&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <beans xmlns"http://www.springframework.org…

解决VSCode中文乱码问题

解决VSCode乱码问题 1.问题描述&#xff1a;2.原因分析&#xff1a;3.解决方案&#xff1a;1&#xff09;解决步骤2&#xff09;深入分析 4.总结 1.问题描述&#xff1a; 最近用vscode的时候突然发现中文字符出现乱码。在网上找了好几种方法都不行&#xff0c;用各种编码格式打…

【机器学习】八、规则学习

知识图谱与基本概念 基本概念 规则学习定义&#xff1a;从训练数据中学习出一组能用于对未见示例进行判别的规则。 规则定义&#xff1a;规则一般是&#xff1a;语义明确、能描述数据分布所隐含的客观规律或领域概念。 逻辑规则定义&#xff1a;⊕←?1⋀?2⋀?3…⋀??⊕…

file2Udp增量日志转出Udp简介

https://gitee.com/tianjingle/file2udp 很多时候服务产生的日志需要进行汇总&#xff0c;这种统一日志处理的方式有elb&#xff0c;而且很多日志组件也支持日志转出的能力。但是从广义上来说是定制化的&#xff0c;我们需要一个小工具实现tail -f的能力&#xff0c;将增量日志…

2024最新基于物联网单片机毕业设计选题汇总(合集)

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

大数据技术与原理实验报告(MapReduce 初级编程实践)

MapReduce 初级编程实践 验环境&#xff1a; 操作系统&#xff1a;Linux&#xff08;建议Ubuntu16.04&#xff09;&#xff1b; Hadoop版本&#xff1a;3.2.2&#xff1b; &#xff08;一&#xff09;编程实现文件合并和去重操作 对于两个输入文件&#xff0c;即文件 A 和…

upload-labs关卡8(基于黑名单的点绕过)通关思路

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

用示波器测量高压电

示波器本身是不可以测试几千v的高压电电路的&#xff0c;一般自带的探头衰减倍数不够&#xff0c;需要使用高压差分探头或者高压探棒&#xff0c;将测试信号衰减到合适的范围再接入示波器。 普通探头能测差分电压吗&#xff1f;差分探头和普通探头有什么区别&#xff1f;全网最…

使用SQL分析数据科学职业发展趋势

大家好&#xff0c;在数据成为新石油的今天&#xff0c;了解数据科学职业的细微差别比以往任何时候都更加重要。无论你是正在寻找机会的数据爱好者&#xff0c;还是资深数据专家&#xff0c;使用SQL都可以让你深入了解数据科学就业市场。 本文可以带你了解哪些数据科学职位最具…

C语言精选练习题:(10)字幂数

每日一言 信心这个东西&#xff0c;什么时候都像个高楼大厦&#xff0c;但是里面会长白蚁。 --沉默的大多数 题目 打印1~100000之间的所有的字幂数&#xff0c;中间用空格分隔 (字幂数是指一个数的位数的n次方之和等于这个数本身) 如&#xff1a;370 3 ^ 3 7 ^ 3 0 ^ 3&…

CCC数字钥匙设计 --数字钥匙数据结构

1、数字钥匙是什么&#xff1f; 汽车数字钥匙&#xff0c;将传统实体钥匙数字化&#xff0c;用卡片、手机等智能设备来做数字钥匙的载体。 从而实现无钥匙进入/启动、为他人远程钥匙授权、个性化的车辆设置等功能。 目前市场上流行的数字钥匙方案是通过NFC、BLE、UWB通信技术…

C51--串口发送中断请求

在串口通信寄存器中有T1&#xff1a;发送中断请求标志位 换行 &#xff1a;\r\n #include "reg52.h" #include <intrins.h>sfr AUXR 0x8E;//串口初始化 void UartInit(void) //9600bps11.0592MHz {PCON & 0x7F; //波特率不倍速SCON 0x50; //8位数…

eNSP毕业设计系列-《大型企业网》-BGP网络无nat

客户主要需求&#xff1a;需要有三栋楼&#xff0c;每栋楼有三个业务。 又新增了要求&#xff0c;要双核心、双防火墙。 所以我根据客户的需求&#xff0c;完成了如下组网设计。 主要技术&#xff0c;MSTPVRRP链路聚合OSPF传统纵网&#xff0c;&#xff08;万金油组合&#…

AI 绘画 | Stable Diffusion WebUI的基本设置和插件扩展

前言 Stable Diffusion WebUI是一个基于Gradio库的浏览器界面&#xff0c;用于配置和生成AI绘画作品&#xff0c;并且进行各种精细地配置。它支持目前主流的开源AI绘画模型&#xff0c;例如NovelAI/Stable Diffusion。 在基本设置方面&#xff0c;Stable Diffusion WebUI的默…

ViewPager2和TabLayout协同使用

一、ViewPager2的基本用法 使用前先添加依赖&#xff1a; implementation androidx.appcompat:appcompat:1.4.0 // AndroidX AppCompatimplementation com.google.android.material:material:1.4.0 // Material Design Components1、制作Fragment 首先制作一个Fragment的xml布…

Mysql数据库管理---MySQL数据库连接、权限认证

1 mysql系统连接权限认证。 1 mysql数据库权限表在数据库启动时就载入内存&#xff0c;当用户通过身份验证后&#xff0c;就在内存中进行相应权限的存取。系统会用到mysql数据库中3个核心表&#xff1a;user&#xff0c;host&#xff0c;db。 主要包括&#xff1a; 用户列&a…

【Opencv】图像融合addWeighted()函数示例和详解

本文通过原理和示例对addWeighted&#xff08;&#xff09;函数进行详解&#xff0c;&#xff0c;并通过改变融合系数展示多个结果&#xff0c;帮助大家理解和使用。 目录 函数原理示例权重&#xff08;0.5,0.5&#xff09;权重&#xff08;0.8,0.2&#xff09;权重&#xff08…

如何解决Windows电脑 Create folder error,Access is denied.

如何解决 Create folder error, Error: mkdir C:\Program Files\nodejs\21.1.0/: Access is denied. Waring: Name : http://npm.taobao.org/mirrors/node/v21.1.0/win-x64/node.exe Code : -2 Error : Create folder error, Error: mkdir C:\Program Files\nodejs\\21.1.0/…

【Linux】虚拟机连不上外网 (ping www.baidu.com不通)

进入linux系统&#xff0c;打开终端&#xff0c;ping www.baidu.com 发现ping不通 首先我连接的是nat模式 查看是否连接上自己本机的网 切换root用户 使用 ifconfig 命令查看是eth0 还是 ens33 vi /etc/sysconfig/network-scripts/ifcfg-ens33 BOOTPROTOstatic ONBOOTyes …