【JavaEE精炼宝库】多线程(7)定时器

目录

一、定时器的概念

二、标准库中的定时器

三、自己实现一个定时器

[3.1 MyTimerTask 实现:](#3.1 MyTimerTask 实现:)

[3.2 MyTimer 实现:](#3.2 MyTimer 实现:)


一、定时器的概念

定时器也是软件开发中的⼀个重要组件。类似于一个 "闹钟"。达到一个设定的时间之后,就执行某个指定好的代码(可以用来完成线程池里面的非核心线程的超时回收)。

定时器是一种实际开发中非常常用的组件。 比如网络通信中,如果对方 500ms 内没有返回数据,则断开连接尝试重连。比如⼀个 Map,希望里面的某个 key 在 3s 之后过期(自动删除)。类似于这样的场景就需要用到定时器。

二、标准库中的定时器

标准库中提供了⼀个 Timer 类。Timer 类的核心方法为 schedule。

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

其中第一个参数 TimerTask 是一个抽象类,本质上还是实现了 Runnable 接口,所以我们就可以把它当作 Runnable 来使用即可。

• 使用演示:

java 复制代码
public class Main {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 3000");
            }
        },3000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 2000");
            }
        },2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 1000");
            }
        },1000);
    }
}

案例效果演示:

会间隔对应的时间打印。

三、自己实现一个定时器

我们在写代码之前要想好我们的需求是什么,也就是我们要实现什么,我们定时器的需求:1. 是能够延时执行任务 / 指定时间执行任务。2. 能够管理多个任务。

3.1 MyTimerTask 实现:

首先我们先实现任务,可以实现 Runnable 接口,或者采用把 Runnable 作为类参数,来进行实现。这里我们采用把 Runnable 作为类参数来进行实现。为了起到延时的效果,我们还需要一个 time 参数来保存绝对的时间。

为什么需要绝对时间呢?

答:这个其实很好理解,我们举一个栗子来解释:假如现在是早上 9 点,领导让你 1 小时之后去找他,也就是说我们应该在早上 10 点左右去找他,但是如果我们只是记录 1 个小时,那么随着时间的推移,我们不能够知道这个 1 小时之后,是哪个时间点的。当然我们可以采用倒计时的方法来实现,但是这样我们还要不停的维护,这个倒计时,倒不如直接记录绝对时间来的简单。

这里我们还需要实现 Comparable 接口,为什么还需要实现这个接口呢?

答:在 Timer 类中,任务不仅仅只有一个,且绝对时间大小与进入队列的顺序没有绝对关系,那么我们如何在队列中快速找到绝对时间最小的任务呢(如果绝对时间最小的任务都不满足执行时间,那么后面的任务绝对也不满足)?显然需要使用到优先级队列(小根堆)来存储任务,但是我们自定义的类不能比较,所以我们需要实现 Comparable 接口来重写 CompareTo 方法。

具体代码如下:

java 复制代码
class MyTimerTask implements Comparable<MyTimerTask> {
    private long time;//绝对时间
    private Runnable runnable;//加上 private 体现封装性

    public MyTimerTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time + System.currentTimeMillis();//绝对时间
    }

    public void run() {//方便后续调用
        runnable.run();
    }
    public long getTime(){
        return time;
    }

    //重写比较器,从小到大排序
    @Override
    public int compareTo(MyTimerTask o) {
        return this.time >= o.time ? 1 : -1;
    }
}

3.2 MyTimer 实现:

这个就是我们要实现的定时器,通过上面在 MyTimerTask 的分析可知,我们这里需要优先级队列来辅助管理任务。同时还需要一个线程来不停的执行队列中的任务,并且还要提供一个 schedule 方法。所以总共要实现的东西有:

• 线程

• 优先级队列(小根堆)

• schedule 方法

• 保证线程安全(通过使用锁的方式要实现)

注意:这里我们不使用 Java 自带的优先级阻塞队列,原因是:优先级阻塞队列本身内部就有一个锁,我们为了保证线程安全,外面还要加一层锁,如果使用阻塞队列,那就是两个锁嵌套的情况,一不小心就会出现死锁的情况,所以倒不如我们同一处理,只使用一个锁即可。在自己实现阻塞队列的时候不能使用 continue 来循环等待("忙等"),这样很消耗 CPU 资源,也不能使用 sleep 来进行阻塞,因为 sleep 不能释放锁(抱着锁睡),线程睡了就真的睡了,综上我们选择采用 wait 的方式来进行阻塞。

还有一些小细节在代码中都有标注释,这里就不再赘述了。

具体代码如下:

• 大体框架:

线程一直不停的扫描队首元素,看看是否能执行这个任务。

java 复制代码
class MyTimer {
    private Object locker = new Object();
    //不用阻塞优先级队列,因为有两个锁,一不小心就死锁了
    private PriorityQueue<MyTimerTask> heap = new PriorityQueue<MyTimerTask>();//因为有实现 comparable 所以 不用再传入比较器
    public MyTimer(){
        Thread thread = new Thread(() -> {
            try{
                while (true) {
                    synchronized (locker) {
                        if (heap.isEmpty()) {
                            locker.wait();
                        }
                        MyTimerTask tmp = heap.peek();
                        long curTime = System.currentTimeMillis();
                        if (curTime >= tmp.getTime()) {
                            //执行
                            tmp.run();
                            heap.poll();
                        } else {
                            //时间还未到
                            locker.wait(tmp.getTime() - curTime);
                        }
                    }
                }
            }catch(InterruptedException e){//把异常统一处理
                throw new RuntimeException(e);
            }
        });
        thread.start();//线程启动
    }
}

• schedule 方法:

Timer 类提供的核心方法为 schedule,用于注册一个任务,并指定这个任务多长时间后执行。这里加上锁有两个原因:1. 保证线程安全。2. 唤醒执行队列元素线程(如果在 wait 中的话)。

java 复制代码
public void schedule(Runnable runnable,long delay){
        synchronized(locker){
            MyTimerTask task = new MyTimerTask(runnable,delay);
            heap.add(task);
            locker.notify();//这里必须要唤醒一下,因为添加新的任务后,绝对时间最小的不一定就是栈顶元素,要把新加入的元素一起考虑一下。
        }
    }

• 验证正确性:

还是上面在演示标准库 Timer 那里的案例。

java 复制代码
public class demo1 {

    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 3000");
            }
        },3000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 2000");
            }
        },2000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 1000");
            }
        },1000);
    }
}

案例演示效果如下:

可以看到符合我们预期的效果🎉🎉🎉

我们现在实现的是单线程的定时器,也可以实现多线程的定时器,只需要加个 List 来管理多个线程即可。

这里可能有的友友就有疑问:这样保证不了准时执行,在一个时间段,如果任务非常多的情况下。

其实无论如何都无法保证时间准的,只要你短时间内插入海量的任务,超过了 CPU 能够负担的极限都会不准,我们最多能做的就是把误差控制到一定的范围(加硬件)。

结语:

其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。

相关推荐
pedestrian_h11 小时前
Java单例模式回顾
java·单例模式·设计模式
a8a30211 小时前
Spring Boot(快速上手)
java·spring boot·后端
华科易迅11 小时前
MybatisPlus乐观锁
java·开发语言·mybatis
G探险者11 小时前
如何找到那些慢 SQL
java
迈巴赫车主11 小时前
错位排序算法
开发语言·数据结构·算法·排序算法
zzb158011 小时前
Agent记忆与检索
java·人工智能·python·学习·ai
炽烈小老头11 小时前
【每日天学习一点算法 2026/03/31】不同路径
学习·算法
计算机安禾11 小时前
【数据结构与算法】第17篇:串(String)的高级模式匹配:KMP算法
c语言·数据结构·学习·算法·visual studio code·visual studio·myeclipse
羊小猪~~11 小时前
【QT】-- 模型与视图简介
开发语言·数据库·c++·后端·qt·前端框架·个人开发
叶微信11 小时前
Qt相关面试题
开发语言·qt