【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 能够负担的极限都会不准,我们最多能做的就是把误差控制到一定的范围(加硬件)。

结语:

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

相关推荐
P7进阶路几秒前
实现用户登录系统的前后端开发
java
2401_857617621 分钟前
“无缝购物体验”:跨平台网上购物商城的设计与实现
java·开发语言·前端·安全·架构·php
觅远2 分钟前
python+reportlab创建PDF文件
开发语言·python·pdf
thesky1234562 分钟前
活着就好20241226
学习·算法
事业运财运爆棚4 分钟前
7种server的服务器处理结构模型
java·linux·服务器
我曾经是个程序员18 分钟前
C#File文件基础操作大全
开发语言·c#
西岭千秋雪_20 分钟前
设计模式の中介者&发布订阅&备忘录模式
java·观察者模式·设计模式·中介者模式·备忘录模式
林浔090623 分钟前
QT信号槽
开发语言·qt
憶巷26 分钟前
MyBatis中动态SQL执行原理
java·sql·mybatis
重生之绝世牛码27 分钟前
Java设计模式 —— 【结构型模式】享元模式(Flyweight Pattern) 详解
java·大数据·开发语言·设计模式·享元模式·设计原则