Java当中的定时器

news2025/2/25 15:24:38

目录

一、什么是定时器

二、Java当中的定时器

①schedule()方法:

②TimerTask

​编辑

  ③delay

三、实现一个定时器       

前提条件:

代码实现:

  ①确定一个“任务”(MyTask)的描述:

   ②schedule方法:

   ③需要一个计时器

        属性:

        构造方法:

         存在问题分析1:忙等:

      解决"忙等":

     存在问题分析二:notify()相比于wait()提前唤醒

        


一、什么是定时器

   给一个非常常见的场景:

   当我们在玩游戏,与其他玩家进行对战的时候,往往会出现下面的一些场景:

    我们点击了"vs"按键,进入了对战加载的页面。

    

 此时,如果在系统加载的过程当中,一直卡顿在某个时刻,无法前行。那么,难道程序就一直僵持住,无法行动了吗?


       并不是的,这个时候,后台会有一个"计时器“,如果加载的时间超过了设定的时间之后,就执行其他的任务。

       因此,总结一下,定时器本质就是一个拥有延时执行任务功能的工具。当达到指定的时间之后,可以执行定时器内部的任务。


二、Java当中的定时器

       Java当中就有一个定时器Timer.


①schedule()方法:

       定时器当中的核心方法就是:schedule()方法。

       这个方法,用于注册一个任务,并指定这个任务在调用schedule方法延时多长时间执行任务。


      可以看到,这个方法有两个参数:TimerTask task和long delay:下面将分析一下这两个参数


②TimerTask

   点进去这个类,可以看到这个是一个抽象类。

      Task的中文翻译就是任务。同时,这个抽象类也实现了Runnable接口,并且也把Runnable()接口当中的run方法继承了

     这说明TimeTask这个类描述了定时器需要执行的任务。


  ③delay

       确定延时的时间。当传入一个数字参数的时候,传入的毫秒数为这个定时器需要在调用schedule()方法之后多久执行自己的任务

     


    代码实现(采用匿名内部类的方式实现):

 public static void main(String[] args) {
        System.out.println("程序启动");
        Timer timer=new Timer();
        //schedule()方法是”安排“的意思
        //给定时器制定一个”任务“
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行定时器的任务1");
            }
        },3000);
    }

   运行程序,可以看到,TimerTask当中的run方法在调用schedule方法之后的3000毫秒内被执行了:

    


三、实现一个定时器       

      场景描述,有3个线程,分别延时3000ms,2000ms,1000ms。

      如果想让延时越少的线程越先执行,那么应该如何初始化线程呢?

      首先,初始化延时3000ms任务的定时器,再初始化2000ms任务的定时器,最后初始化1000ms任务的定时器。

      这样,就可以让延时最短的线程最先执行:

             


    运行结果:


前提条件:

如果想自定义一个定时器来管理需要执行的任务,那么就需要下面两个条件:

 ①让注册的任务在指定的时间内被执行

 ②注册的多个任务按照最初计划的时间运行

   详细描述: 

A.需要一个数据结构,来保存一开始创建的N个任务。

B.我们这里指定的任务,都是带个"时间" 。即:多久之后才执行任务。

C.那么,就可以考虑使用优先级队列(PriorityQueue).

    按照时间的大小,建立小根据。

     时间越少的任务,优先级越高,越先执行。


代码实现:

       ①确定一个“任务”(MyTask)的描述:

        封装两个属性,一个是Runnable接口的引用,另外一个是delay,用于确定任务执行的时间:

        同时,也要描述出这个任务的"优先级"。因此要让这个"任务”实现comparable接口,拥有可以比较的能力:明确任务的优先级

  class MyTask implements Comparable<MyTask>{
    /**
     * 需要执行的任务
     */
    private Runnable runnable;
    /**
     * 任务在什么时候执行:时间戳+延时时间
     */
    private long delay;

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

    /**
     * 获取延长时间
     * 延长的时间@return
     */
    public long getDelay() {
        return delay;
    }

    /**
     * 执行任务
     */
    public void run(){
        runnable.run();
    }
    /**
     * 实现一个比较器,让
     * 这个任务是可以比较的
     * 待比较的对象@param o
     * 确定小根堆的比较方式
     * @return
     */
    @Override
    public int compareTo(MyTask o) {
        //this比o小,那么返回小于0
        //谁的时间小,谁先执行
        return (int) (this.getDelay()-o.getDelay());
    }
}


 ②schedule方法:

  指定需要执行的任务,以及当前任务延迟多久执行:并且把任务存放到优先级阻塞队列(PriorityBlockingQueue当中)

/**
     * 指定两个参数
     * 第一个表示任务@param runnable
     * 第二个表示多久之后执行@param after
     */
    public void schedule(Runnable runnable,long after){
        MyTask myTask=new MyTask(runnable,System.currentTimeMillis()+after);
        queue.put(myTask);
    }

 


  ③需要一个计时器

        属性:

         其中一个是存放任务的优先级阻塞队列(PriorityBlockingQueue):

         调用了schedule()方法之后传入的任务的优先队列
         优先级高(等待时间少)的任务先执行


 

        另外一个是扫描线程t


         扫描线程t的作用:

         一般情况下面,一个定时器(Timer)只需要初始化一次,然后就可以存放多个任务到定时器当中。

         扫描线程由于是在构造方法当中初始化的,那么,也就意味着扫描线程也只需要初始化一次。

         它的作用就是不断从阻塞队列当中取出任务(Task),并且执行。

        

         如果没有扫描线程,那么也就意味着一切的任务(Task)都需要由主线程来执行。


      代码实现: 

    /**
     * 扫描线程
     * 扫描线程:用于在构造方法当中初始化,并且执行从阻塞队列当中循环取出任务的线程
     *
     */
    private Thread t;
    /**
     * 一个阻塞的优先队列,用于保存
     * 调用了schedule()方法之后传入的任务的优先队列
     * 优先级高(等待时间少)的任务先执行
     */
    private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();

        构造方法:

          A.初始化扫描线程t,并且指定它对应的任务

          B.在线程t的任务当中,需要不断循环从优先级的阻塞队列当中取出MyTask。

 

         并且进行判断:

         ①当前时间没有达到任务需要执行的时间的时候,就把任务放回优先级阻塞队列当中。

         ②如果当前时间已经达到了任务需要执行的时间,那么就执行对应的任务,调用myTask.run()方法。


 public MyTimer(){
        t=new Thread(()->{
            while (true){
                 //取出队首元素,检查看看队首元素任务是否时间到了
                try {
                    MyTask myTask=queue.take();
                    //如果时间没有到,继续把任务放回到队列当中
                    //获取现在的时间
                    long curTime=System.currentTimeMillis();
                    //拿现在的时间和myTask需要执行指定的时间进行对比
                    //如果现在的时间没有到需要执行的时间,那么不执行任务,把这个任务放回到阻塞队列当中
                    if(curTime<myTask.getDelay()){
                        queue.put(myTask);
                    }else {
                        //如果到了需要执行的时间,那么执行对应的任务
                        myTask.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

         存在问题分析1:忙等:

         当前时刻,如果达到了任务需要执行的时间,那么的确可以执行任务。

         如果没有达到任务需要执行的时间,那么,会不断循环地出现:

         把任务(task)从阻塞队列当中取出来,然后再把任务(task)放到阻塞队列当中的现象。

         由于我们这里使用的是优先级队列,因此,当把这个任务再次取出来的时候,仍然是堆顶的元素。

    

        这样的话, 假如一个任务需要在14:00执行,但是现在的时刻为13:00,远远没有到达需要执行的时间,那么,在这1h之内,将执行数以10亿次的循环,并且这个循环都是没有意义的。


        这种现象,在计算机当中,被称为"忙等"。也就是,等着任务执行,但是在等的这个过程当中,并没有"闲着”,而是不断重复没有意义的工作。

       按道理来说,等待的过程需要释放CPU资源的,但是这里并没有释放,因此造成了CPU资源的浪费。


      解决"忙等":

       针对上述代码,不要再执行忙等了,而是"阻塞式“等待。

       描述:

       当任务从计时器当中被取出来的时候,如果没有到达任务需要执行的时间,那么就让扫描线程进入阻塞等待的状态,等待的时间为当前时间与线程规定的时间之差

       也就是说,如果当前任务没有到达执行的时间,那么就需要等待到执行的时间。


       此处,可以考虑使用wait(timeout)或者sleep(timeout)两种方式来进行阻塞等待。那使用哪种方式比较好呢?

       使用wait()方法比较好,原因如下:

       如果使用了Thread.sleep(timeout)方法,那么也就意味着,扫描线程需要等待到timeout时间了,才可以重新工作。

       如果在这个期间,其他任务(MyTask)也被添加到了计时器当中,那么,它们将无法在timeout时间之前执行。

      

       因为,所有添加的任务执行,都是需要通过扫描线程来执行的。既然扫描线程都要wait()到对应的时间执行,那么新添加的任务,也不能执行了。


       因此,需要使用wait(timeout)方法来代替Thread.sleep(timeout)来执行对应的任务。

       当定时器当中有新的任务添加的时候,把扫描线程notify()了。

     代码实现: 


 

 

 


     存在问题分析二:notify()相比于wait()提前唤醒

     假如现在的时间为13:00

      在上图的代码当中,定时器存放了两个任务,其中一个任务是延时3000秒执行,另外一个是延时2000秒执行。

      假如,这一个定时器被创建好之后。某一时刻,扫描线程从定时器当中取出一个任务(MyTask)   


       当取出来之后(也就是红色箭头指向的部分),读取到了任务的执行时间为14:00,被判定为没有到指定的执行时间。

       但是,此时扫描线程被操作系统调度离开了cpu内核。


       此时,又有一个新的线程,调用了schedule()方法,往定时器当中存放任务。指定任务的执行时间为13:30。

       根据上面的写法,新的线程在执行schedule()方法快结束的时候,会notify()唤醒正在等待的扫描线程。


        图解:

时间轴扫描线程添加任务的线程
t1取出任务
t2判定时间,没有到达指定的时间
t3被调度离开cpu内核
t4调用schedule()方法添加任务到阻塞队列当中,并且notify()
t5回到cpu内核,并且执行wait(timeout)

         可以看到,新添加任务的线程,比扫描线程先执行了notify()方法。

         那么,也就意味着,t5时刻,执行的等待时间,是从13:00开始的,需要等待1h。那么,新添加的任务,也就是需要在14:30执行的任务,无法正常执行了。


        那如何解决这个问题,也就是让wait()方法优先执行呢?

       那就是,让"取出任务","判定时间","执行wait()"这三个操作变成原子的,保证take()和wait()操作之间不要有其他任务"贸然闯入"。

       图解:

时间轴扫描线程添加任务的线程
t1取出任务
t2判定时间,没有到达指定的时间
t3进行等待(timeout),wait()
t4添加任务
t5唤醒扫描线程:notify()

 

        代码实现:

     


 

        

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

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

相关文章

MAT-内存泄漏工具使用

目录 一、MAT简介 1.1 MAT介绍 1.2 MAT工具的下载安装 二、使用MAT基本流程 2.1 获取HPROF文件 2.2 MAT主界面介绍 2.3 MAT中的概念介绍 2.3.1 Shallow heap 2.3.2 Retained Heap 2.3.3 GC Root 2.4 MAT中的一些常用的视图 2.4.1 Thread OvewView 2.4.2 Group 2.…

复杂工况下少样本轴承故障诊断的元学习

摘要&#xff1a;近年来&#xff0c;基于深度学习的轴承故障诊断得到了较为系统的研究。但是&#xff0c;这些方法中的大多数的成功在很大程度上依赖于大量的标记数据&#xff0c;而这些标记数据在实际生产环境中并不总是可用的。如何在有限的数据条件下训练出鲁棒的轴承故障诊…

线程状态到底是5种还是六种?傻傻分不清楚

目录 从操作系统层面上描述线程状态 从javaAPI层面上理解线程的6种状态 线程的状态转换. NEW --> RUNNABLE 1.RUNNABLE <--> WAITING 2.RUNNABLE <--> WAITING 3.RUNNABLE <--> WAITING 1.RUNNABLE <--> TIMED_WAITING 2.RUNNABLE <--&…

开源天气时钟项目删减和更新

开源天气时钟项目删减和更新&#x1f4cc;原项目开源地址&#xff1a;https://gitee.com/liuzewen/ESP8266-SSD1306-Watch-mini ✨本文只针对Arduino IDE平台代码进行删减和更新。 &#x1f4fa;按键菜单功能 &#x1f33c;天气时钟功能整体架构描述 代码中所使用的库&…

【MySQL】十,SQL执行流程

MySQL中的SQL执行流程 MySQL的查询流程 查询缓存 Server 如果在查询缓存中发现了这条 SQL 语句&#xff0c;就会直接将结果返回给客户端 如果没有&#xff0c;就进入到解析阶段&#xff08;MySQL 8.0 已经废弃了查询缓存功能&#xff09;。 解析器 在解析器中对 SQL 语句进行…

36、基于STM32的电子闹钟(DS1302)

编号&#xff1a;36 基于STM32的电子闹钟&#xff08;DS1302&#xff09; 功能描述&#xff1a; 本设计由STM32单片机液晶1602按键DS1302时钟模块声光报警模组成。 1、采用STM32F103最小系统。 2、利用DS1302芯片提供时钟信号 3、液晶1602实时显示年月日、时分秒、星期等信息…

java线程池原理

背景&#xff1a;为什么需要线程池java中的线程池是运用场景最多的并发框架&#xff0c;几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中&#xff0c;合理的使用线程池能够带来3个好处。降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消…

(1分钟了解)SLAM的七大问题:地图表示、信息感知、数据关联、定位与构图、回环检测、深度、绑架

编辑切换为居中添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09;SLAM问题也被称为是CML问题。编辑切换为居中添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09;编辑切换为居中添加图片注释&#xff0c;不超过 140 字&#xff08;可选&…

(JMLR-2019)NAS综述鼻祖-神经架构搜索:一项调查

神经架构搜索&#xff1a;一项调查 paper题目&#xff1a;Neural Architecture Search: A Survey paper是博世人工智能中心发表在JMLR 2019的工作 paper链接&#xff1a;地址 Abstract 过去几年&#xff0c;深度学习在图像识别、语音识别和机器翻译等各种任务上取得了显着进步…

【C++】stl---vector的模拟实现(超级详细,万字详解)

文章目录前言vector的成员属性构造函数size函数cacpcity函数begin和end函数reserve函数insert函数push_back函数[]操作符重载析构函数拷贝构造函数赋值操作符重载erase函数pop_back反向迭代器反向迭代器模板反向迭代器的构造函数运算符重载- -运算符重载*引用操作符重载&#x…

Spring AOP 企业级应用 - 统一功能处理

1.统一用户登录权限效验统一用户登录权限效验使用传统的 AOP 能否解决问题呢 ? Component Aspect // 标识当前类为一个切面 public class LoginAOP {// 定义切点 (拦截的规则) - 拦截 UserController 中的所有方法Pointcut("execution(* com.example.demo.controller.Tes…

React Hooks 基础、实现、原理

React Hooks 基础、实现、原理题外话为什么要有Hooks&#xff1f;但是Class Component 的用法也有缺陷&#xff1a;1.组件复用变的困难2.JavaScript本身的缺陷函数式React HooksuseStateuseEffectuseCallback、useMemouseReducer最后题外话 2023了&#xff0c;新年快乐&#x…

【javascript】DOM 案例

点击查看密码 &#xff1a;就是把type等于password改为text即可&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><…

电力系统强大的Gurobi 求解器的学习(PythonMatlab)

到底有多强大&#xff0c;看看就知道&#xff0c;必须&#x1f44d;&#x1f44d;&#x1f44d;&#xff1a; 目录 1 概述 2 算例理解【Python】 2.1 算例1——详细入门 2.2 算例2——一般线性规划问题 2.3 算例3——非凸问题 3 算例升级【Matlab】 3.1 模型 3.2 电力系统…

Python2.x 与 3​​.x 版本到底有啥区别?

嗨害大家好鸭&#xff01;我是小熊猫~ 今天给大家带来一点小干货~ 很多人对于python的版本有些许疑问&#xff0c; 今天就来给大家说说看~ Python学习资料电子书点击此处跳转文末名片 Python 的 3​​.0 版本&#xff0c;常被称为 Python 3000&#xff0c;或简称 Py3k。 相对…

Mybatis-Plus“读-批量写-读”数据不一致的问题分享

在日常开发过程中&#xff0c;时常会遇到一个如下场景&#xff1a; 根据条件x&#xff0c;读取表A&#xff0c;得到多行数据&#xff1b;遍历读取到的数据&#xff0c;对条件x以外的字段进行修改&#xff0c;并进行保存&#xff1b;&#xff08;重点&#xff09;修改后&#x…

基础算法(七)——离散化

离散化 介绍 这里的离散化&#xff0c;特指整数的、保序的离散化 有些题目可能需要以数据作为下标来操作&#xff0c;但题目给出的数据的值比较大&#xff0c;但是数据个数比较小。此时就需要将数据映射到和数据个数数量级相同的区间&#xff0c;这就是离散化&#xff0c;即…

基于imx6ull第一个Linux驱动

在编译第一个驱动之前&#xff0c;需要把基本的环境准备好&#xff0c;可以参照这两篇文章&#xff1a;https://wlink.blog.csdn.net/article/details/128590747https://wlink.blog.csdn.net/article/details/128591216我们之前写过一个基于ubuntu最基本的字符设备驱动&#xf…

关于固态硬盘冷数据掉速问题解决方案

20230107 By wdhuag 前言&#xff1a; 我有一个西数蓝盘500G固态&#xff0c;系统盘&#xff0c;一年没开机&#xff0c;这个月开机后发现系统很卡&#xff0c;持续读取假死严重。测试没有坏块&#xff0c;网上说的是冷数据掉速问题。 参考&#xff1a; 如何看待西数/闪迪多…

排序算法:插入、希尔、选择、冒泡

目录 一.插入排序 1.算法描述&#xff1a; 2.实现思路&#xff1a; 3.时间复杂度&#xff1a; 代码如下&#xff1a; 二.希尔排序 &#xff08;插入排序的优化升级&#xff09; 1.算法描述&#xff1a; 2.实现思路&#xff1a; 3.时间复杂度&#xff1a; 代码如下&a…