模拟实现Java中的计时器

定时器是什么

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

定时器是⼀种实际开发中⾮常常⽤的组件. ⽐如⽹络通信中, 如果对⽅ 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("程序开始运行");
    }
}

退出 退出当前程序,所有计时任务将不被执行。 注:1.21版之后可以设置“禁止退出”,启用了“禁止退出”设置该操作将无效。 结束 关闭 即时,关闭计算机或操作系统。 重启 即时,重启计算机操作系统。 注销 即时,注销当前用户。 结束→关闭 关闭系统 即时,正常关闭操作系统(与操作系统软关机类似)。 windows 2000 操作系统环境下该操作只能退出系统到“可以安全关闭电源”。 强关系统 即时,强行中止所有应用程序,并关闭操作系统。 windows 2000 操作系统环境下该操作只能退出系统到“可以安全关闭电源”。 关闭电源 即时,正常关闭操作系统并关闭计算机电源(需要ATX电源支持)。 强关电源 即时,强行中止所有应用程序,并关闭操作系统与计算机电源(需要ATX电源支持)。 结束→重启 重启系统 即时,正常重新启动操作系统(与操作系统重启类似)。 强制重启 即时,强行中止所有应用程序,并重新启动操作系统。 结束→注销 注销用户 即时,正常注销当前系统用户(与操作系统注销类似)。 强制注销 即时,强行中止所有应用程序,并注销当前系统用户。 维护 关闭显示器 即时,关闭显示器。 锁定工作台 即时,锁定当前用户工作台,回到系统用户登录界面。 电源管理器 进行计算机电源管理操作,包括待机与休眠。 维护→电源管理器 待机 即时,进入计算机待机状态。 当前处于运行状态的数据保存在内存中,机器只对内存供电,而硬盘、屏幕和CPU等部件则停止供电。 由于数据存储在速度快的内存中,因此进入等待状态和唤醒的速度比较快。 不过这些数据是保存在内存中,如果断电则会使数据丢失。 休眠 即时,进入计算机休眠状态。 将当前处于运行状态的数据保存在硬盘中,整机将完全停止供电。 因为数据存储在硬盘中,而硬盘速度要比内存低得多,所以进入休眠状态和唤醒的速度都相对较慢,在休眠时可以完全断开电脑的电源。 添加任务 关闭 添加,计时关闭计算机或操作系统任务。 重启 添加,计时重启计算机操作系统任务。 注销 添加,计时注销当前用户任务。 维护 添加,计时计算机操作系统维护任务。 运行 添加,计时运行任务。 闹铃 添加,计时闹铃任务。 注:请在任务执行前,在 设置 选项的“闹铃设置”选择执行闹钟任务时播放的音频文件。 添加任务→关闭 关闭系统 添加,计时正常关闭操作系统(与操作系统软关机类似)任务。 windows 2000 操作系统环境下该操作只能退出系统到“可以安全关闭电源”。 强关系统 添加,计时强行中止所有应用程序,并关闭操作系统任务。 windows 2000 操作系统环境下该操作只能退出系统到“可以安全关闭电源”。 关闭电源 添加,计时正常关闭操作系统并关闭计算机电源(需要ATX电源支持)任务。 强关电源 添加,计时强行中止所有应用程序,并关闭操作系统与计算机电源(需要ATX电源支持)任务。 添加任务→重启 重启系统 添加,计时正常重新启动操作系统(与操作系统重启类似)任务。 强制重启 添加,计时强行中止所有应用程序,并重新启动操作系统任务。 添加任务→注销 注销用户 添加,计时正常注销当前系统用户(与操作系统注销类似)任务。 强制注销 添加,计时强行中止所有应用程序,并注销当前系统用户任务。 添加任务→维护 关闭显示器 添加,计时关闭显示器任务。 锁定工作台 添加,计时锁定当前用户工作台,回到系统用户登录界面任务。 电源管理器 添加,计时进行计算机电源管理操作,待机与休眠任务。 添加任务→动行 应用程序 添加,计时启动设置应用程序任务。 1.执行文件,选择要让时启动的可执行程序。 如果是在系统的默认目录则可直接输入可执行名称。 如:cmd或cmd.exe 2.运行参数,应用程序运行所需的参数,如果没有则为空。 如/c ping 127.0.0.1 添加任务→维护→电源管理器 待机 添加,计时计算机进入待机状态任务。 当前处于运行状态的数据保存在内存中,机器只对内存供电,而硬盘、屏幕和CPU等部件则停止供电。 由于数据存储在速度快的内存中,因此进入等待状态和唤醒的速度比较快。 不过这些数据是保存在内存中,如果断电则会使数据丢失。 休眠 添加,计时计算机进入休眠状态任务。 将当前处于运行状态的数据保存在硬盘中,整机将完全停止供电。 因为数据存储在硬盘中,而硬盘速度要比内存低得多,所以进入休眠状态和唤醒的速度都相对较慢,在休眠时可以完全断开电脑的电源。 任务计时方式 日期时间 在给定的日期时间执行指定的任务。必须输入当前日期时间之后的日期时间,否则无法添加。 工作日 在选择的工作日给定的时间执行指定的任务。如果全选则每天都会在给定的时间执行任务,只要您在给定的时间启动了该程序。 (显示任务剩余时间时,如果当前工作日未选择或给定时间已过时,则不显示剩余时间。) 倒计时 以秒为单位倒计时执行指定的任务。输入倒计时数时以分钟为单位输入。 任务列表 任务列表 任务列表,显示待执行的计时任务。 1.日期时间类型任务,可双击查看剩余时间。被执行后任务自动删除。 2.工作日类型任务,可双击查看剩余时间(如果当前工作日未被选择则无剩余时间显示)。任务不会被自动删除。 3.倒计时类型任务,可双击查看剩余时间。被执行后任务自动删除。 注:1.2.1版本后新增了常驻任(如:工作日任务)务类型可设置“启用”和“跳过本次”选项。 启用:取消该选项则该任务不会被执行。 跳过本次:选择该选项将跳过当日当前任务将不被执行(工具重启后该选项就失效,需要重新设置)。 删除任务 删除列表中被选中的任务。 设置 闹铃设置 设置闹铃的声音。选择执行闹钟任务时播放的音频文件,可试听正确后保存设置。 禁止退出设置 启用该选项后工具的所有退出操作将无效,启用后菜单会有“√”标志。 其它 基它功能说明 1.2.1版本后, 1.工具支持后台启动,只要在exe后加“-h”启动参数,如:WindowMinor.exe -h 2.解决了重复启动时不能唤出程序的问题,如果启动时没有带“-h”启动参数, 则不管已经启动的工具属于什么状态都会唤出操作界面, 如带启动参数了“-h”则在不管是当前工具属于什么状态都退出操作界面进入后台运行状态。 资源配置 java虚拟机配置 在默认情况下,程序启动时会使用自身的jre(以下所说的jar为java虚拟机1.6.0以上版本,低版本则无法启动程序)。 在主目录(安装目录、程序所在的目录)的“jre1.6”目录下,如果没有则需要在主目录创建“jre1.6”目录, 并将jre安装目录下的“bin”与“lib”目录复制到该目录下。 如果启动时没有找到该jre,则查找windows系统配置的jre,如果还是没有找到或版本过低,则无法启动。 资源配置文件名 在程序的主目录(安装目录、程序所在的目录)创建“res.properties”文件。 如果没有改文件则所有配置使用默认值。 端口 1.db4o对象数据库(保存任务与历史记录)连接的端口号,“db4o.port=值”, 如果不配置或配置错误则在 1100-9999 这个范围内使用一个可用的端口, 如果最终无可用端口则退出程序。 2.RMI服务(RIM远程唤出),绑定服务的端口号,“rmi.port=值”, 如果不配置或配置错误则在 1100-9999 这个范围内使用一个可用的端口, 如果最终无可用端口则抛出异常,继续运行工具,但无法远程唤出工具。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

早点睡觉1.0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值