javaEE 初阶 — 定时器

news2025/2/27 2:44:56

文章目录

  • 定时器
    • 1 什么是定时器
    • 2 标准库中定时器
    • 3 实现一个定时器
      • 3.1 实现的思路
      • 3.2 为什么要使用优先级队列来保存任务
      • 3.3 开始实现

定时器

1 什么是定时器


定时器 类似于一个 “闹钟”,达到一个设定的时间之后,就执行某个指定好的代码。

定时器是一种实际开发中非常常用的组件。

比如网络通信中,如果对方 500ms 内没有返回数据,则断开连接尝试重连。
比如一个 Map,希望里面的某个 key 在 3s 之后过期(自动删除)。

类似于这样的场景就需要用到定时器。

2 标准库中定时器


Timer 这个类就是标准库的定时器

 Timer timer = new Timer();

定时器使用

package thread;

import java.util.Timer;
import java.util.TimerTask;

public class ThreadDemo4 {

    public static void main(String[] args) {
        // Timer 这个类就是标准库的定时器
        System.out.println("程序启动!");
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定时器任务启动");
            }
        },5000); //5000毫秒后执行 run 方法中的任务
    }
}




在等待了5000毫秒后,就执行了定时器的任务。

schedule 这个方法的效果是给定时器注册一个任务。
但是这个任务不会立即执行,而是在指定时间进行执行。

3 实现一个定时器

3.1 实现的思路


1、让被注册的任务能够在指定时间被执行

  • 单独在定时器内部搞个线程,让这个线程周期性的扫描,判定任务是否到时间了。
  • 如果到时间了,就执行;没到时间就等等。

2、一个定时器是可以注册多个任务的,这多个任务会按照约定时间按顺序执行

  • 这里的多个任务需要使用 优先级队列 来保存。

3.2 为什么要使用优先级队列来保存任务


定时器里的每一个任务都是带有 “时间” 概念的,也就是多长时间过后就执行。
可以肯定的是,时间越靠前的越先执行。

可以把时间小的,作为优先级最高。
此时的队首元素就是整个队列中最要先执行的任务。

此时只需要扫描线程扫描队首元素即可,而不必遍历整个队列。
因为如果队首元素还没到执行的时间,后续的元素就更不可能到执行的时间。

3.3 开始实现


1、我们可以使用标准库中带有阻塞功能的优先级队列: PriorityBlockingQueue 来保存
要执行的任务。

定义一个类来表示我们要执行的任务和执行的时间

//表示定时器中的任务
class MyTask {
    //任务执行的内容
    private Runnable runnable;

    //执行的时间 - 毫秒时间戳表示
    private Long time;

    //构造方法
    public MyTask(Runnable runnable, Long time) {
        this.runnable = runnable;
        this.time = time;
    }

    //获取当前任务的时间
    public Long getTime() {
        return time;
    }

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


此时 MyTask 就是要保存在 PriorityBlockingQueue 中的任务

class MyTimer {
    //扫描线程
    private Thread search = null;

    //保存任务的阻塞优先级队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
}


2、定时器类需要注册一个 “schedule” 方法来注册任务

我们期望这里保存的是一个 绝对时间,而 after 是一个像 1000ms 这样的毫秒级时间,
一个时间间隔。

所以需要使用当前的时间戳加上 System.currentTimeMillis() 得到一个是在什么时间去执行的标准时间戳。

//第一个参数是任务内容
//第二个参数是任务在多少毫秒之后执行
public void schedule(Runnable runnable, Long after) {
    //注意这里的时间换算
    MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);
    queue.put(task); //填到队列当中
}


3、如何实现扫描线程的主要逻辑

1、因为使用的是 优先级队列,所以这里只要取出队首元素即可。

  MyTask myTask = queue.take();


2、计算出当前的时间

 Long curTime = System.currentTimeMillis();


3、如果到了执行任务的时间就执行,没到就把任务重新塞回队列中

 if (curTime < myTask.getTime()) {
     // 要把任务塞回到队列中
     queue.put(myTask);
 } else { // 到执行任务时间了
     // 执行任务
     myTask.run();
 }


完整代码

 //构造方法里创建一个线程
 public MyTimer() {
     search = 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();
             }
         }
     });
     search.start();
 }


上述代码存在的两个问题

1、没有指定 MyTask 怎么比较优先级

现在执行两个任务看一下状况

 public static void main(String[] args) throws InterruptedException{
     MyTimer myTimer = new MyTimer();
     myTimer.schedule(new Runnable() {
         @Override
         public void run() {
             System.out.println("任务1");
         }
     }, 1000);

     myTimer.schedule(new Runnable() {
         @Override
         public void run() {
             System.out.println("任务2");
         }
     }, 2000);
 }




Comparable 用来描述比较规则的接口,这里提示我们还没有描述规则的 Comparable 接口。

可以让 MyTask 类实现 Comparable 接口
或者也可以使用 Comparable 单独写一 个比较器

下面是实现一个 Comparable

class MyTask implements Comparable<MyTask> {
    @Override
    public int compareTo(MyTask o) {
        // 这里会返回 <0 >0 =0 三种结果
        // this 比 o 小 返回 <0
        // this 比 o 大 返回 >0
        // this 等于 o 返回 =0
        return (int) (this.time - o.time); // 因为时间是long类型的,所以返回需要强制类型转换
    }
}




2、如果执行的时间没到就会一直重复取出来塞进去的操作(忙等

按理说,等待是要释放 CPU 资源的,让 CPU 资源可以干别的事情。
但是忙等,即进行了等待,又占用着 CPU 资源。

就像是有的人虽然今天休假,但是一会又要线上开会,一会又要打扫卫生。
自己还没怎么休息,但是一天就过去了,自己虽然是在休假,但是也没有闲着。

如果此时还没到任务执行的时间,比如说任务执行的时间是 14:00, 但是现在是 13:00
那么在这个时间段内,上述代码的循环操作就可能会被执行数十亿次,甚至更多。

就好比 18:00 就下课了,但是此时是 17:30 ,我过一会看一下时间,过一会看一下时间。
虽然是在等待着下课时间的到来,但是我也没有闲着。


针对上述的情况,不要在忙等了,而是要进行阻塞式等待。
可以使用 sleep 或者 wait

不使用 sleep 的原因:

  • 随时都有可能有新的任务到来,如果新任务执行的时间更早呢。
    也就是说这里等待的时间不明确。

  • 如果新的任务执行的时间是 30 分钟后,但是 sleep 设置的时间是 1个小时,
    那么这个时候就会错过这个任务。

使用 wait 更合适,更方便随时唤醒。
如果有新的任务来了就 notify 唤醒,然后在检查一下时间,重新计算要等待的时间。
而且 wait 也提供了一个带有 “超时时间” 的版本

带有超时时间的 wait 就可以保证:

  • 当新任务来的时候,随时 notify 唤醒
  • 如果没有新任务,则最多等到之前旧任务中的最早任务时间到,就会被唤醒。

在 put 操作之后 进行 wait,还要搭配锁来使用。

在 schedule 方法里进行唤醒(notify)

synchronized (this) {
    this.wait(myTask.getTime() - curTime); // 得到需要等待的时间
}

// 唤醒wait
synchronized (this) {
    this.notify();
}


此时的代码还有一个和线程随机调度相关的问题

假设代码执行到了 ** queue.put(myTask);** ,这个线程就要从 cpu 调度走了。
当线程回来之后,接下来就要进行 wait 操作了,此时 wait 的时间已经是算好的。

比如当前时间是 13:00 ,任务时间是 14:00 ,即将要 wait 1 小时。(此时还没有执行wait)
如果此时有另一个线程调用了 schedule 方法添加新任务,新任务是 13:30 执行。

由于 扫描线程 wait 还没执行呢,所以此处的 notify 只是会空打一炮,
不会产生任何的唤醒操作。

此时此刻,新的任务虽然已经插入到队列,新的任务也是在队首,
紧接着,扫描线程回到 cpu 了,此时等待的时间仍然是 1 小时。

因此,13:30 的任务就被错过了。


了解了上述问题之后就不难发现,问题出现的原因,是因为当前 take 操作和 wait 操作不是原子的。

如果在 take 和 wait 之间加上锁,保证在这个过程中不会有新的任务过来,问题自然解决。

 //构造方法里创建一个线程
 public MyTimer() {
     search = new Thread(() -> {
         while (true) {
             try {
                 // 取出队首元素,检查队首元素任务是否到时间了
                 // 如果没到时间,就把任务重新放到队列中
                 // 如果到时间了,就执行任务
                 synchronized (this) {
                     MyTask myTask = queue.take(); //拿出队首元素
                     long curTime = System.currentTimeMillis(); //计算当前的时间
                     // 还没到执行的时间
                     if (curTime < myTask.getTime()) {
                         // 要把任务塞回到队列中
                         queue.put(myTask);
                         // put 之后进行 wait
                         this.wait(myTask.getTime() - curTime); // 得到需要等待的时间
                     } else { // 到执行任务时间了
                         // 执行任务
                         myTask.run();
                     }
                 }
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     });
     search.start();
 }

完整代码

package thread;

import java.util.concurrent.PriorityBlockingQueue;

//表示定时器中的任务
class MyTask implements Comparable<MyTask> {
    //任务执行的内容
    private Runnable runnable;

    //执行的时间 - 毫秒时间戳表示
    private long time;

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


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

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

    @Override
    public int compareTo(MyTask o) {
        // 这里会返回 <0 >0 =0 三种结果
        // this 比 o 小 返回 <0
        // this 比 o 大 返回 >0
        // this 等于 o 返回 =0
        return (int) (this.time - o.time); // 因为时间是long类型的,所以返回需要强制类型转换
    }
}

class MyTimer {
    //扫描线程
    private Thread search = null;

    //保存任务的阻塞优先级队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    //构造方法里创建一个线程
    public MyTimer() {
        search = new Thread(() -> {
            while (true) {
                try {
                    // 取出队首元素,检查队首元素任务是否到时间了
                    // 如果没到时间,就把任务重新放到队列中
                    // 如果到时间了,就执行任务
                    synchronized (this) {
                        MyTask myTask = queue.take(); //拿出队首元素
                        long curTime = System.currentTimeMillis(); //计算当前的时间
                        // 还没到执行的时间
                        if (curTime < myTask.getTime()) {
                            // 要把任务塞回到队列中
                            queue.put(myTask);
                            // put 之后进行 wait
                            this.wait(myTask.getTime() - curTime); // 得到需要等待的时间
                        } else { // 到执行任务时间了
                            // 执行任务
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        search.start();
    }

    //第一个参数是任务内容
    //第二个参数是任务在多少毫秒之后执行
    public void schedule(Runnable runnable, long after) {
        //注意这里的时间换算
        MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);
        queue.put(task); //填到队列当中

        // 唤醒wait
        synchronized (this) {
            this.notify();
        }
    }
}

public class ThreadDemo5 {

    public static void main(String[] args) throws InterruptedException {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1");
            }
        }, 1000);

        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2");
            }
        }, 2000);
    }
}

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

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

相关文章

印染行业APS智能排程排产的应用意义

不得不说的印染之“痛” 在印染行业&#xff0c;因排产无法自动化、智能化&#xff0c;企业在交期、成本、生产管理方面承受着巨大的压力&#xff0c;尤其当下印染企业生产管理正从传统的粗放式转向精细化&#xff0c;这些痛点愈加凸显。 一方面&#xff0c;客户和企业面临一个…

httpd安装

一、离线安装 1、去 https://pkgs.org/ 下载httpd所依赖的7个rpm包 [基于CentOS 7 x86_64系统&#xff0c;如需其他环境可前往官网直接下载] apr-1.4.8-5.el7.x86_64.rpm apr-util-1.5.2-6.el7.x86_64.rpm apr-util-ldap-1.5.2-6.el7.x86_64.rpm postgresql-libs-9.2.24-1.el…

互联互通-标准化成熟度指标分析(未完成)

整体分析1 医疗机构基本情况2 数据资源标准化建设情况&#xff08;30 分&#xff09;2.1数据集标准化情况&#xff08;15 分&#xff09;2.1.1电子病历基本数据集 第1部分&#xff1a;病历概要&#xff08;1-4数据集&#xff09;2.1.2电子病历基本数据集 第2部分&#xff1a;门…

Jetpack Compose UI创建布局绘制流程+原理 —— 内含概念详解(手撕源码)

本文是我去年首发于稀土掘金平台的文章 全文较长&#xff1a;共1万5千字&#xff0c;适合有耐心❤️的人学习 有些概念不懂的可以去4.部分概念详解这个目录先稍微学习一下 Compose源码基于最新的Compose 版本&#xff1a;1.0.1 系统源码基于最新的Android11 版本 注意&#xff…

【蓝桥杯基础题】2020年省赛填空题—回文日期

&#x1f451;专栏内容&#xff1a;蓝桥杯刷题⛪个人主页&#xff1a;子夜的星的主页&#x1f495;座右铭&#xff1a;前路未远&#xff0c;步履不停 目录一、题目背景二、题目描述1.问题描述2.输入格式3.输出格式4.一个例子5. 评测用例规模与约定三、题目分析1.获取位数2.回文…

236页10万字精选数据中台建设方案2022版

【版权声明】本资料来源网络&#xff0c;知识分享&#xff0c;仅供个人学习&#xff0c;请勿商用。【侵删致歉】如有侵权请联系小编&#xff0c;将在收到信息后第一时间删除&#xff01;完整资料领取见文末&#xff0c;部分资料内容&#xff1a; 目录 1. 数据中台平台建设方案 …

数据存储大小端 网络字节序

一、概念 大端模式&#xff1a;数据的低位存放在内存的高地址中 小端模式&#xff1a;数据的低位存放在内存的低地址中 二、数据的高低位 首先需要清楚一段数据存储高低位区分 联想记忆最右边为16^0 下来16^1 比如下图二进制为例&#xff1a; 三、内存的高低位 以vs2019为…

JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配

文章目录前言一、排序规范1、happens-before原则2、找文档位置二、对象创建的过程&#xff08;后面回答的就是这几个问题&#xff09;1、一线互联网企业面试题&#xff1a; 关于对象2、对象创建过程三、对象在内存中的存储布局1、1.8版本虚拟机配置2、对象的内存布局a、普通对象…

字节三面:如何设计一个高并发系统

前言 大家好&#xff0c;我是田螺。 记得很久之前&#xff0c;去面试过字节跳动。被三面的面试官问了一道场景设计题目&#xff1a;如何设计一个高并发系统。当时我回答得比较粗糙&#xff0c;最近回想起来&#xff0c;所以整理了设计高并发系统的15个锦囊&#xff0c;相信大…

【EHub_tx1_tx2_E100】Ubuntu18.04 + ROS_ Melodic + 万集716 单线激光 测试

简介&#xff1a;介绍 万集716 单线激光 在EHub_tx1_tx2_E100载板&#xff0c;TX1核心模块环境&#xff08;Ubuntu18.04&#xff09;下测试ROS驱动&#xff0c;打开使用RVIZ 查看点云数据&#xff0c;本文的前提条件是你的TX1里已经安装了ROS版本&#xff1a;Melodic。关于测试…

三、k8s资源管理

文章目录1 k8s资源管理介绍2 YAML语言介绍3 资源管理方式3.1 命令式对象管理3.2 命令式对象配置3.3 声明式对象配置3.4 如何编写YAML1 k8s资源管理介绍 在kubernetes中&#xff0c;所有的内容都抽象为资源&#xff0c;用户需要通过操作资源来管理kubernetes。 kubernetes的本质…

MySQL字符集和排序规则详解

一. 相关概念1. 字符集MySQL提供了多种字符集和排序规则选择&#xff0c;其中字符集设置和数据存储以及客户端与MySQL实例的交互相关&#xff0c;排序规则和字符串的对比规则相关(1). 字符集的设置可以在MySQL实例、数据库、表、列四个级别(2). MySQL设置字符集支持在InnoDB, M…

Git学习:工作流学习实践

文章目录一、前言二、开发过程一、前言 在实践的项目开发过程中&#xff0c;会使用Git或者类似的版本控制工具来管理代码。下面介绍基于Git工具在实际项目开发过程中的使用流程。 如上图所示显示了项目开发的一个简化流程。在开发一个新需求/版本的时候&#xff0c;一般会从主…

筛法求欧拉函数

欧拉函数的定义 在数论中&#xff0c;对正整数n&#xff0c;欧拉函数是小于n的正整数中与n互质的数的数目. 欧拉函数的重要性质 若(即m与n互质)&#xff0c;则若为质数&#xff0c;则若为质数&#xff0c;则对于性质2&#xff0c;若为质数&#xff0c;则小于的个数都互质&am…

北大陈斌Python算法笔记(二)

前言 &#x1f340;作者简介&#xff1a;被吉师散养、喜欢前端、学过后端、练过CTF、玩过DOS、不喜欢java的不知名学生。 &#x1f341;个人主页&#xff1a;红中 &#x1f342;不就是蓝桥杯嘛&#xff0c;干他&#xff01;&#xff01;我堂堂 栈的应用&#xff1a;简单括号匹…

docker安装nginx与容器之间的互相通信

目录 1. docker网络模式 2. 连接容器的三种方法 3. Docker Networking 3.1 创建网络 3.2 查看宿主机中创建的网络 3.3 删除网络 3.4 如何使用网络 4.搭建Nginx 1.准备工作 1.1 拉取镜像 ​编辑1.2 在宿主机中创建挂载目录 2.准备2个tomcat 容器集群 3.准备 Nginx配…

力扣(LeetCode)1658. 将 x 减到 0 的最小操作数(C++/Python)

题目描述 逆向思维滑动窗口 题目分析 &#xff1a; 从数组左侧和右侧&#xff0c;取出左侧的连续数字&#xff0c;右侧的连续数字&#xff0c;使得这些数字之和等于 x&#xff0c;维护最小取数次数&#xff0c;作为答案 。 设整个数组之和 total &#xff0c;除去左侧和右侧的…

Notes 12.0.2版本新特性

大家好&#xff0c;才是真的好。 随着上周代号多瑙河的Notes/Domino 12.0.2版本正式发布&#xff0c;很多人在周末加班&#xff0c;写新特性和功能测试文档等&#xff0c;恩&#xff0c;我也是这样&#xff0c;所以今天第一时间来介绍下Notes 12.0.2客户机新特性。 从12.0.2版…

GC中的一些技术

OopMap&#xff08;ordinary object pointer map&#xff09; 记录时机 JIT编译时在特定的位置&#xff08;安全点/安全区&#xff09;记录下OopMap&#xff0c;记录了执行到该方法的某条指令的时候&#xff0c;栈上和寄存器里哪些位置是引用类加载动作完成时&#xff0c;Hot…

关于sql注入这一篇就够了

本文章根据b站迪总课程总结出来,若有不足请见谅 目录 本文章根据b站迪总课程总结出来,若有不足请见谅 存在sql注入条件 判断数据库类型 注入mysql思路 判断网站是否存在注入点 判断列名数量&#xff08;字段数&#xff09; 文件读写操作 网站路径获取方法 注入类型 按注入点数据…