Java中的定时器(Timer)

news2024/11/13 14:41:29

目录

一、什么是定时器?

二、标准库中的定时器

三、实现定时器


一、什么是定时器?

定时器就像一个"闹钟",当它到达设定的时间后,就会执行预定的代码。

例如,我们在TCP的超时重传机制中讲过,如果服务器在规定的时间内没有收到客户端返回的ACK确认应答,那么它将再次发送请求。

二、标准库中的定时器

1.标准库中提供了一个Timer类,Timer类的核心方法为schedule;

2.schedule包含两个参数,第一个参数为即将要执行的任务代码,第二个参数为指定多长时间之后执行(单位为毫秒)

    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("Timer");
            }
        },3000);
    }

三、实现自定义定时器

为了实现一个自定义的计时器,我们需要满足以下两个条件:

  1. 被调度的任务可以按照指定时间执行;
  2. 一个定时器可以调度多个任务,并按照最初约定的时间执行它们。

1.被调度的任务可以按照指定时间执行。

       针对第一个条件,我们可以创建一个线程来周期性地扫描任务列表,检查每个任务是否到达指定的执行时间。如果任务到达了预定的执行时间,就执行相应的代码;如果没有达到预定的执行时间,就不执行任务。

2.一个定时器可以调度多个任务,并按照最初约定的时间执行它们。

       针对第二个条件,我们可以使用一个优先级队列(PriorityBlockingQueue),这里为了线程安全我们使用PriorityBlockingQueue,而不是PriorityQueue来保存所有的任务。这个队列可以根据任务的执行时间进行排序,使得时间最早的任务位于队列的前端,即最先要执行的任务。这样,在第一个条件中描述的扫描线程只需要检查队列的首元素即可,而不需要遍历整个任务列表。

 这里还需要处理一个小问题,就是我们如何描述一个任务?我们可以使用Runnable描述任务,并添加一个表示执行时间的字段。

class MyTask{
    //要指定的任务
    private Runnable runnable;
    ///任务执行时间(毫秒时间戳)
    private long time;

    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }

    /**
     * 获取当前任务时间
     *
     * @return
     */
    public long getTime() {
        return time;
    }

    /**
     * 执行任务
     */
    public void run(){
        runnable.run();
    }
}

 我们按照上面的两个条件的描述,写出下列代码:

class MyTimer {
    //扫描线程
    private Thread t = null;
    //优先级队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    public MyTimer() {
        t = new Thread(() -> {
            while (true) {
                try {
                    //取出队首元素,判断当前任务是否到达时间
                    MyTask myTask = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (curTime > myTask.getTime()) {
                        //未到达时间,放回队列中
                        queue.put(myTask);
                    } else {
                        //到达时间,执行任务
                        myTask.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * @param runnable
     * @param time
     */
    public void schedule(Runnable runnable, long time) {
        MyTask task = new MyTask(runnable, System.currentTimeMillis() + time);
        queue.offer(task);
    }
}

上面的代码看起来很有道理,我们运行代码

 然后就会得到报错,在优先级队列中第五点的使用示例我们详细说明过原因,这里不再赘述,简而言之,当你使用自定义类作为PriorityQueue的元素时,除了提供一个比较器(Comparator)来定义元素之间的排序规则外,你还可以通过实现Comparable接口并重写compareTo方法来定义元素的自然顺序,我们需要明确说明,当前任务对象的优先级是什么样的

class MyTask implements Comparable<MyTask> {
    //要指定的任务
    private Runnable runnable;
    ///任务执行时间(毫秒时间戳)
    private long time;

    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }

    /**
     * 获取当前任务时间
     *
     * @return
     */
    public long getTime() {
        return time;
    }

    /**
     * 执行任务
     */
    public void run() {
        runnable.run();
    }

    @Override
    public int compareTo(MyTask o) {
        return (int) (this.time - o.getTime());
    }
}

但是上面的代码还存在一个问题,如果当前任务未到执行时间时,代码会不断重复执行队列的取出和塞回操作。

       如果当前任务未到执行时间时,代码会不断重复执行队列的取出和塞回操作,这种现象被称为"忙等"。为了更有效地利用CPU资源,我们需要使用阻塞式等待而不是忙等。

       在这种情况下,我们知道等待的时间比较明确,第一时间想到了使用sleep方法来等待,但是可能会出现问题。例如,如果我们添加了一个比之前添加的任务更早的任务,那么可能会错过新任务的执行时间。

       因此,我们可以使用wait方法来实现阻塞式等待更为合适,因为它可以更方便地唤醒线程并重新检查时间。

       wait方法还提供了一个带有"超时时间"的版本,这意味着我们可以指定一个最长等待时间,以避免无限期地等待。这样,即使没有新的任务加入,线程也可以在一定时间后自动唤醒并继续执行其他任务。

class MyTimer {
    //扫描线程
    private Thread t = null;
    //优先级队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    public MyTimer() {
        t = new Thread(() -> {
            while (true) {
                try {
                    //取出队首元素,判断当前任务是否到达时间
                    MyTask myTask = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (curTime > myTask.getTime()) {
                        //未到达时间,放回队列中
                        queue.put(myTask);
                        synchronized (this) {
                            this.wait(myTask.getTime() - curTime);
                        }
                    } else {
                        //到达时间,执行任务
                        myTask.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * @param runnable
     * @param time
     */
    public void schedule(Runnable runnable, long time) {
        MyTask task = new MyTask(runnable, System.currentTimeMillis() + time);
        queue.offer(task);
        synchronized (this) {
            this.notify();
        }
    }
}

上面的代码看起来已经很完备了,但是其实还有一个很严重的问题,这个问题和线程安全/随机调度有关。

       我们考虑一个极端情况,假设代码执行到 `queue.put(myTask);` 这一行时,当前线程被CPU调度走。当线程回来之后,接下来就需要进行等待操作,此时等待时间已经计算好了。

       例如,当前时间为19:30,任务执行时间为20:00,即将要等待30分钟。但是此时的wait还没有开始执行,而在这个时候,另一个线程调用了schedule方法,添加了一个新任务,新任务的执行时间为19:45。然后就会调用notify方法通知等待唤醒,但是令人遗憾的是,扫描线程的等待还没有开始执行,所以这里的notify通知是无效的,不会产生任何唤醒操作

       此时此刻,新的任务已经插入队列,并且位于队首,但是当前的等待时间仍然是30分钟,导致19:45的任务就被错过了。

 

       在上面的说明中,我们可以发现问题出现的原因是:在take和wait的操作之间存在一个时间窗口,在这个时间窗口内,如果有新的任务被添加,那么扫描线程可能会错过这个新任务。

       为了解决这个问题,我们需要确保take和wait的操作是原子的,即在执行这两个操作时,不允许有其他线程插入新的任务,在这里我们可以通过扩大锁的范围,来避免这个问题。

    public MyTimer() {
        t = new Thread(() -> {
            while (true) {
                try {
                    synchronized (this) {
                        //取出队首元素,判断当前任务是否到达时间
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (curTime > myTask.getTime()) {
                            //未到达时间,放回队列中
                            queue.put(myTask);

                            this.wait(myTask.getTime() - curTime);
                        } else {
                            //到达时间,执行任务
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

好了,到这里实现自定义定时器代码已经结束了。

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

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

相关文章

XDMA - AXI4 Memory Mapped

目录 1. What is SG DMA2. Descriptor3. Transfer for H2CStep 1. The host prepares stored data and creates descriptors in main memoryStep 2. The host enables DMA interruptsStep 2. The driver initializes DMA with descriptor start addressStep 3. The driver writ…

数据结构(邓俊辉)学习笔记】串 06——KMP算法:构造next[]表

文章目录 1. 递推2. 算法3. 实现 1. 递推 接下来的这节&#xff0c;我们就来讨论 next 查询表的构造算法。我们将会看到非常有意思是&#xff0c; next 表的构造过程与 KMP 主算法的流程在本质上是完全一样的。 在这里&#xff0c;我们不妨采用递推策略。我们只需回答这样一个…

带你深入浅出新面经:十六、十大排序之快速排序

此为面经第十六谈&#xff01;关注我&#xff0c;每日带你深入浅出一个新面经。 我们要了解面经要如何“说”&#xff01; 很重要&#xff01;很重要&#xff01;很重要&#xff01; 我们通常采取总-分-总方式来阐述&#xff01;&#xff08;有些知识点&#xff0c;你可以去…

python脚本请求数量达到上限,http请求重试问题例子解析

在使用Python的requests库进行HTTP请求时&#xff0c;可能会遇到请求数量达到上限&#xff0c;导致Max retries exceeded with URL的错误。这通常发生在网络连接不稳定、服务器限制请求次数、或请求参数设置错误的情况下。以下是一些解决该问题的策略&#xff1a; 增加重试次数…

【负载均衡式在线OJ】项目设计

文章目录 程序源码用到的技术项目宏观结构代码编写思路 程序源码 https://gitee.com/not-a-stupid-child/online-judge 用到的技术 C STL 标准库。Boost 准标准库(字符串切割)。cpp-httplib 第三方开源网络库。ctemplate 第三方开源前端网页渲染库。jsoncpp 第三方开源序列化…

栈和队列有何区别?

栈和队列是两种常见的数据结构&#xff0c;它们分别用于解决不同类型的问题。在程序设计中&#xff0c;栈和队列都是非常重要的数据结构&#xff0c;因为它们可以帮助我们解决很多实际的问题。 栈&#xff1a; 首先&#xff0c;让我们来讨论栈, 栈是一种后进先出&#xff08;…

学NLP不看这本书等于白学!一书弄懂NLP自然语言处理(附文档)

随着人工智能技术的飞速发展&#xff0c;自然语言处理成为了计算机科学与人工智能领域中不可或缺的关键技术之一。作为一名长期致力于人工智能和自然语言处理研究的学者&#xff0c;今天给大家推荐的这本《自然语言处理&#xff1a;大模型理论与实践》正是学NLP自然语言非常牛逼…

黑神话悟空用什么编程语言

《黑神话&#xff1a;悟空》作为一款备受瞩目的国产单机动作游戏&#xff0c;其背后的开发涉及了多种编程语言和技术。根据公开信息和游戏开发行业的普遍做法&#xff0c;可以推测该游戏主要使用了以下几种编程语言&#xff1a; C&#xff1a; 核心编程语言&#xff1a;作为《黑…

【C++ Primer Plus习题】5.7

问题: 解答: #include <iostream> #include <string> using namespace std;typedef struct _Car {string brand;int year; }Car;int main() {int count 0;cout << "请问你家有多少辆车呢?" << endl;cin >> count;cin.get();Car* ca…

Java 入门指南:Java IO流 —— 序列化与反序列化

序列化 序列化是指将对象转换为字节流的过程&#xff0c;以便能够将其存储到文件、内存、网络传输等介质中&#xff0c;或者在不同的进程、网络或机器之间进行数据交换。 序列化的逆过程称为反序列化&#xff0c;即将字节流转换为对象。过反序列化&#xff0c;可以从存储介质…

【mysql】mysql之索引学习

本站以分享各种运维经验和运维所需要的技能为主 《python零基础入门》&#xff1a;python零基础入门学习 《python运维脚本》&#xff1a; python运维脚本实践 《shell》&#xff1a;shell学习 《terraform》持续更新中&#xff1a;terraform_Aws学习零基础入门到最佳实战 《k8…

面试搜狐大型模型算法工程师,感受非凡体验!

搜狐大模型算法工程师面试题 应聘岗位&#xff1a;搜狐大模型算法工程师 面试轮数&#xff1a; 整体面试感觉&#xff1a;偏简单 面试过程回顾 1. 自我介绍 在自我介绍环节&#xff0c;我清晰地阐述了个人基本信息、教育背景、工作经历和技能特长&#xff0c;展示了自信和沟通…

【Office】激活文件无法打开-DragonKMS--解决办法

【解决办法】右键 文件属性>>最下面勾选解除锁定即可打开。 【原因】&#xff1a;网络上下载的文件&#xff08;包括exe、zip等&#xff09;。

vue.js3+element-plus+typescript add,edit,del,search

vite.config.ts server: {cors: true, // 默认启用并允许任何源host: 0.0.0.0, // 这个用于启动port: 5110, // 指定启动端口open: true, //启动后是否自动打开浏览器 proxy: {/api: {target: http://localhost:8081/, //实际请求地址&#xff0c;数据库的rest APIschangeOr…

esp32 控制 st7735s 显示屏(spi)

Lcd初始化后全屏为花屏&#xff0c;必须再把整个屏幕转成全底白色消除花屏后再显示图片&#xff0c;字符。 我理解为什么是花屏&#xff0c;因为只是初始化各个参数&#xff0c;显示内存现在还是为空&#xff0c;还没有执行0x2c命令。 图片 #include "driver/spi_master…

统一 transformer 与 diffusion !Meta 融合新方法剑指下一代多模态王者

本文引入了 Transfusion&#xff0c;这是一种可以在离散和连续数据上训练多模态模型的方法。 来源丨机器之心 一般来说&#xff0c;多模态生成模型需要能够感知、处理和生成离散元素&#xff08;如文本或代码&#xff09;和连续元素&#xff08;如图像、音频和视频数据&#xf…

软件测试-Selenium+python自动化测试

目录 一、元素定位 1.1一个简单的模板 1.2单选框radio定位实战 1.3下拉操作 1.4弹窗 1.5文件上传 1.6 iframe(类似于页中页,嵌套进去了) 二、元素定位实战 会用到谷歌浏览器Chrome测试,需要下载一个Chromedriver(Chrome for Testing availability)对应自己的浏览…

力扣面试经典算法150题:除自身以外数组的乘积

除自身以外数组的乘积算法详解 今天的题目是力扣面试经典150题中的数组的中等难度题&#xff1a;除自身以外数组的乘积。 题目链接&#xff1a;https://leetcode.cn/problems/product-of-array-except-self/description/?envTypestudy-plan-v2&envIdtop-interview-150 …

docker基础到进阶

基础 文章目录 基础1.Docker简介2.Docker基础概念3.Docker安装4.Docker命令4.1 镜像命令4.2 容器命令 5. 数据卷5.1具名挂载5.2 匿名挂载 进阶1. 镜像5.2 Dockerfile5.3 网络1.网络模式2.网络操作 DockerCompose1.基本语法 总结 这篇文章记录了以下的内容&#xff1a; 1️⃣ 利…

达梦数据库的系统视图v$object_usage

达梦数据库的系统视图v$object_usage 在达梦数据库&#xff08;DM Database&#xff09;中&#xff0c;V$OBJECT_USAGE 视图提供了关于数据库对象的使用情况和统计信息。这些对象可以包括表、索引、视图、存储过程等。通过 V$OBJECT_USAGE 视图&#xff0c;数据库管理员可以监…