模拟实现Java中的计时器

news2025/2/22 15:54:24

定时器是什么

定时器也是软件开发中的⼀个重要组件. 类似于⼀个 "闹钟". 达到⼀个设定的时间之后, 就执⾏某个指定好的代码. 前端/后端中都会用到计时器.

定时器是⼀种实际开发中⾮常常⽤的组件. ⽐如⽹络通信中, 如果对⽅ 500ms 内没有返回数据, 则断开连接尝试重连. ⽐如⼀个 Map, 希望⾥⾯的某个 key 在 3s 之后过期(⾃动删除). 类似于这样的场景就需要⽤到定时器.

标准库中的定时器

• 标准库中提供了⼀个 Timer 类. Timer 类的核⼼⽅法为 schedule .

• schedule 包含两个参数. 第⼀个参数指定即将要执⾏的任务代码, 第⼆个参数指定多⻓时间之后 执⾏ (单位为毫秒).

// 定时器的使用
public class Demo21 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        // main 方法中调用 timer.schedule 方法时, 
        // 它只是将任务注册到 Timer 中,并告诉 Timer 
        // 在 3000 毫秒后执行这个任务。
        // 任务的执行是由 Timer 内部的守护线程完成的。
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 3");
            }
        }, 3000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 2");
            }
        }, 2000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 1");
            }
        }, 1000);
        System.out.println("程序开始执行!");
    }
}

模拟实现定时器 

那么该怎么解决呢?

 

class MyTimerTask {
    // 任务啥时候执行. 毫秒级的时间戳.
    private long time;
    // 任务具体是啥.
    private Runnable runnable;

    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public MyTimerTask(Runnable runnable, long delay) {
        // delay 是一个相对的时间差. 形如 3000 这样的数值.
        // 构造 time 要根据当前系统时间和 delay 进行构造.
        time = System.currentTimeMillis() + delay;
        this.runnable = runnable;

    }
}

// 定时器的本体
class MyTimer {
    // 使用优先级队列 来保存上述的N个任务
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    // 定时器的核心方法 就是把要执行的任务添加到队列中
    public void schedule(Runnable runnable, long delay) {
        MyTimerTask task = new MyTimerTask(runnable, delay);
        queue.offer(task);
    }
    // MyTimer 中还需要构造一个 "扫描线程", 一方面去负责监控队首元素是否到点了,
    // 是否应该执行;
    // 一方面当任务到点之后,就要调用这里的 Runnable 的 Run 方法来完成任务

    public MyTimer() {
        // 扫描线程
        Thread t1 = new Thread(() -> {
            // 不停地去扫描当前的队首元素
            while (true) {
                try {
                    if (queue.isEmpty()) {
                        continue;
                    }
                    MyTimerTask task = queue.peek();
                    long curTime = System.currentTimeMillis();
                    if (curTime > task.getTime()) {
                        // 假设当前时间是 14:01, 任务时间是 14:00, 
                        // 此时就意味着应该要执行这个任务了.
                        // 需要执行任务.
                        queue.poll();
                        task.getRunnable().run();
                    }else {
                        // 让当前线程休眠一下, 按照时间差来休眠.
                        Thread.sleep(task.getTime() - curTime);
                    }
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
    }
}

上述代码写完了计时器的核心逻辑, 但是这份代码中还有几个关键性的问题. 

最后完整的模拟实现代码.

import java.util.PriorityQueue;
import java.util.Timer;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: xiaotutu
 * Date: 2025-02-20
 * Time: 21:41
 */

class MyTimerTask implements Comparable<MyTimerTask>{
    // 任务啥时候执行. 毫秒级的时间戳.
    private long time;
    // 任务具体是啥.
    private Runnable runnable;

    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public MyTimerTask(Runnable runnable, long delay) {
        // delay 是一个相对的时间差. 形如 3000 这样的数值.
        // 构造 time 要根据当前系统时间和 delay 进行构造.
        time = System.currentTimeMillis() + delay;
        this.runnable = runnable;

    }

    @Override
    public int compareTo(MyTimerTask o) {
        // 认为时间小的, 优先级高. 最终时间最小的元素, 就会放到队首.
        // 怎么记忆, 这里是谁减去谁?? 不要记!! 记容易记错~~
        // 随便写一个顺序, 然后实验一下就行了.
        return (int) (this.time - o.time);
        // return (int) (o.time - this.time);
    }
}

// 定时器的本体
class MyTimer {
    // 使用优先级队列 来保存上述的N个任务
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

    // 用来加锁的对象
    private Object locker = new Object();

    // 定时器的核心方法 就是把要执行的任务添加到队列中
    public void schedule(Runnable runnable, long delay) {
        synchronized (locker) {
            MyTimerTask task = new MyTimerTask(runnable, delay);
            queue.offer(task);
            // 每次来新的任务, 都唤醒一下之前的扫描线程. 
            // 好让扫描线程根据最新的任务情况, 重新规划等待时间.
            locker.notify();
        }
    }
    // MyTimer 中还需要构造一个 "扫描线程", 一方面去负责监控队首元素是否到点了, 
    // 是否应该执行;
    // 一方面当任务到点之后,就要调用这里的 Runnable 的 Run 方法来完成任务

    public MyTimer() {
        // 扫描线程
        Thread t1 = new Thread(() -> {
            // 不停地去扫描当前的队首元素
            while (true) {
                try {
                    synchronized (locker) {
                        while (queue.isEmpty()) {
                            // 注意, 当前如果队列为空, 此时就不应该去取这里的
                            // 元素. 此处使用 wait 等待更合适. 
                            // 如果使用 continue, 就会使这个线程
                            // while 循环运行的飞快,
                            // 也会陷入一个高频占用 cpu 的状态(忙等).
                            //continue;
                            locker.wait();
                        }
                        MyTimerTask task = queue.peek();
                        long curTime = System.currentTimeMillis();
                        if (curTime > task.getTime()) {
                            // 假设当前时间是 14:01, 任务时间是 14:00, 此时就
                            // 意味着应该要执行这个任务了.
                            // 需要执行任务.
                            queue.poll();
                            task.getRunnable().run();
                        }else {
                            // 让当前线程休眠一下, 按照时间差来休眠.
                            // Thread.sleep(task.getTime() - curTime);
                            locker.wait(task.getTime() - curTime);
                        }
                    }
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
    }
}

public class Demo22 {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 3");
            }
        }, 3000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 2");
            }
        }, 2000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 1");
            }
        }, 1000);
        System.out.println("程序开始运行");
    }
}

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

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

相关文章

Ubuntu 的RabbitMQ安装

目录 1.安装Erlang 查看erlang版本 退出命令 2. 安装 RabbitMQ 3.确认安装结果 4.安装RabbitMQ管理界面 5.启动服务并访问 1.启动服务 2.查看服务状态 3.通过IP:port 访问界面 4.添加管理员用户 a&#xff09;添加用户名&#xff1a;admin&#xff0c;密码&#xff1…

vim修改只读文件

现象 解决方案 对于有root权限的用户&#xff0c;在命令行输入 :wq! 即可强制保存退出

【DeepSeek】本地部署,保姆级教程

deepseek网站链接传送门&#xff1a;DeepSeek 在这里主要介绍DeepSeek的两种部署方法&#xff0c;一种是调用API&#xff0c;一种是本地部署。 一、API调用 1.进入网址Cherry Studio - 全能的AI助手选择立即下载 2.安装时位置建议放在其他盘&#xff0c;不要放c盘 3.进入软件后…

NCRE证书构成:全国计算机等级考试证书体系详解

全国计算机等级考试&#xff08;NCRE&#xff09;证书体系为中学生提供了一个系统学习和提升计算机能力的平台。本文将详细介绍 NCRE 证书的构成&#xff0c;帮助中学生了解 NCRE 证书的级别和内容&#xff0c;规划未来职业发展。 一、NCRE 证书体系概述 NCRE 证书共分为四个级…

如何在WPS打开的word、excel文件中,使用AI?

1、百度搜索&#xff1a;Office AI官方下载 或者直接打开网址&#xff1a;https://www.office-ai.cn/static/introductions/officeai/smartdownload.html 打开后会直接提示开始下载中&#xff0c;下载完成后会让其选择下载存放位置&#xff1a; 选择位置&#xff0c;然后命名文…

【设计模式】 代理模式(静态代理、动态代理{JDK动态代理、JDK动态代理与CGLIB动态代理的区别})

代理模式 代理模式是一种结构型设计模式&#xff0c;它提供了一种替代访问的方法&#xff0c;即通过代理对象来间接访问目标对象。代理模式可以在不改变原始类代码的情况下&#xff0c;增加额外的功能&#xff0c;如权限控制、日志记录等。 静态代理 静态代理是指创建的或特…

《A++ 敏捷开发》- 16 评审与结对编程

客户&#xff1a;我们的客户以银行为主&#xff0c;他们很注重质量&#xff0c;所以一直很注重评审。他们对需求评审、代码走查等也很赞同&#xff0c;也能找到缺陷&#xff0c;对提升质量有作用。但他们最困惑的是通过设计评审很难发现缺陷。 我&#xff1a;你听说过敏捷的结对…

NutUI内网离线部署

文章目录 官网拉取源代码到本地仓库修改源代码打包构建nginx反向代理部署访问内网离线地址 在网上找了一圈没有写NutUI内网离线部署的文档&#xff0c;花了1天时间研究下&#xff0c;终于解决了。 对于有在内网离线使用的小伙伴就可以参考使用了 如果还是不会联系UP主:QQ:10927…

【实战篇】【深度介绍 DeepSeek R1 本地/私有化部署大模型常见问题及解决方案】

引言 大家好&#xff01;今天我们来聊聊 DeepSeek R1 的本地/私有化部署大模型。如果你正在考虑或者已经开始了这个项目&#xff0c;那么这篇文章就是为你准备的。我们会详细探讨常见问题及其解决方案&#xff0c;帮助你更好地理解和解决在部署过程中可能遇到的挑战。准备好了…

Qt学习(六) 软件启动界面 ,注册表使用 ,QT绘图, 视图和窗口绘图,Graphics View绘图框架:简易CAD

一 软件启动界面 注册表使用 知识点1&#xff1a;这样创建的界面是不可以拖动的&#xff0c;需要手动创建函数来进行拖动&#xff0c;以下的3个函数是从父类继承过来的函数 virtual void mousePressEvent(QMouseEvent *event);virtual void mouseReleaseEvent(QMouseEvent *eve…

java练习(36)

ps:题目来自力扣 给你一个字符串 s 和一个字符规律 p&#xff0c;请你来实现一个支持 . 和 * 的正则表达式匹配。 . 匹配任意单个字符* 匹配零个或多个前面的那一个元素 所谓匹配&#xff0c;是要涵盖 整个 字符串 s 的&#xff0c;而不是部分字符串。 class Solution {publ…

【Linux网络编程】IP协议格式,解包步骤

目录 解析步骤 1.版本字段&#xff08;大小&#xff1a;4比特位&#xff09; 2.首部长度&#xff08;大小&#xff1a;4比特位&#xff09;&#xff08;单位&#xff1a;4字节&#xff09; &#x1f35c;细节解释&#xff1a; 3.服务类型&#xff08;大小&#xff1a;8比特…

Unity 位图字体

下载Bitmap Font Generator BMFont - AngelCode.com 解压后不用安装直接双击使用 提前设置 1、设置Bit depth为32 Options->Export options 2、清空所选字符 因为我们将在后边导入需要的字符。 Edit->Select all chars 先选择所有字符 Edit->Clear all chars i…

Linux 网络与常用操作(适合开发/运维/网络工程师)

目录 OSI 七层协议简介 应用层 传输层 Linux 命令&#xff01;&#xff01;&#xff01; 1. ifconfig 命令 简介 1. 查看网络地址信息 2. 指定开启、或者关闭网卡 3. 修改、设置 IP 地址 4. 修改机器的 MAC 地址信息 5. 永久修改网络设备信息 2. route 路由命令 …

linux 安装启动zookeeper全过程及遇到的坑

1、下载安装zookeeper 参考文章&#xff1a;https://blog.csdn.net/weixin_48887095/article/details/132397448 2、启动失败 1、启动失败JAVA_HOME is not set and java could not be found in PATH 已安装 JAVA 配置了JAVA_HOME,还是报错解决方法&#xff1a;参考&#xf…

MySQL数据库——表的约束

1.空属性&#xff08;null/not null&#xff09; 两个值&#xff1a;null&#xff08;默认的&#xff09;和not null&#xff08;不为空&#xff09; 数据库默认字段基本都是字段为空&#xff0c;但是实际开发时&#xff0c;尽可能保证字段不为空&#xff0c;因为数据为空没办法…

ArcGIS Pro进行坡度与坡向分析

在地理信息系统中&#xff0c;坡度分析是一项至关重要的空间分析方法&#xff0c;旨在精确计算地表或地形的坡度&#xff0c;为地形特征识别、土地资源规划、环境保护、灾害预警等领域提供科学依据。本文将详细介绍如何利用ArcGIS Pro这一强大的地理信息系统软件&#xff0c;进…

【MySQL常见疑难杂症】MySQL数据库底层图例

● Connectors&#xff08;连接者&#xff09;​&#xff1a;指的是不同语言中与SQL的交互&#xff0c;从图3-1中可以看到目前流行的语言都支持MySQL客户端连接。 ● Connection Pool&#xff08;连接池&#xff09;​&#xff1a;管理缓冲用户连接、线程处理等需要缓存的需求。…

谈谈对线程的认识

面对这样的一个多核CPU时代, 实现并发编程是刚需. 多进程实现并发编程, 效果是特别理想的. 但是, 多线程编程模型有一个明显的缺点, 就是进程太重了, 效率不高. 创建一个进程, 消耗时间比较多. 销毁一个进程, 消耗时间也比较多. 调度一个进程, 消耗时间也比较多. 这里的时…

无人机遥控器接口作用详解!

USB接口&#xff1a; 功能&#xff1a;USB接口是一种通用串行总线接口&#xff0c;用于连接外部设备&#xff0c;如手机、平板、电脑或充电设备。在无人机遥控器上&#xff0c;USB接口通常用于数据传输和充电。 应用&#xff1a;用户可以通过USB接口将遥控器与电脑连接&#…