【多线程】深入剖析定时器的应用

85bef0f9be7b443ebfd3dbba0de889c1.png

💐个人主页初晴~

📚相关专栏:多线程 / javaEE初阶


        在软件开发中,有一些代码逻辑并不需要立马就被执行,可能需要等一段时间在执行。就好像我们会用闹钟来提醒我们过一段时间后要做某事一样,代码中也有“定时器”这种类似于闹钟的机制。那么,计时器都有什么特点,又该如何使用呢,就让博主手把手带你研究一下吧。

一、什么是定时器

定时器也是软件开发中的⼀个重要组件. 类似于⼀个 "闹钟".使其能够在未来的某个时间点或按照预定的时间间隔执行某个指定好的代码。是⼀种实际开发中⾮常常⽤的组件。

4658a6c4cf8b4267ae1e1b11d4118a15.png

常见作用:

1. 计划任务:定时器可以用于执行定期任务,比如数据备份、日志清理、定时发送邮件等。

2. 延迟操作:可以设置一个任务在一段时间后执行,例如,实现一个倒计时功能,在特定时间后触发事件。

3. 定时刷新:在Web应用中,定时器可以用于定时刷新页面或数据,保持与服务器同步的状态。

4. 心跳检测:在网络通信中,定时器可以用于定期发送心跳包,以保持连接活动状态,防止超时断开。

5. 自动更新:可以设置定时器来检查是否有软件更新,如果有的话就自动下载更新包。

6. 提醒服务:在个人应用中,如闹钟、日历应用等,定时器可以用来设置提醒。

7. 资源监控:定时器可以用于定期监控系统资源使用情况,如CPU、内存使用率,然后根据这些信息做出相应的资源管理决策。

8. 性能测试:在软件测试中,可以使用定时器来模拟用户行为,定期发送请求,从而测试系统的响应时间和负载能力。

9. 计时功能:在游戏开发或其他应用中,定时器可用于控制游戏时间或执行计时相关的逻辑。

10. 自动化脚本:定时器可以用于执行批处理脚本或自动化任务,比如定期清理临时文件、统计日志等。

11. 状态维护:某些网络协议要求客户端定期向服务器发送消息来维持连接状态,定时器可以帮助实现这种功能。

二、定时器的实现

实现要点:

1、创建类,描述要执行的任务是啥

2、管理多个任务,通过一定的数据结构,把多个任务存起来

3、用专门的线程去执行这些任务

1、描述任务

我们先创建一个MyTimerTask类,来负责描述所执行的任务

class MyTimerTask{
    private Runnable runnable;
    //通过毫秒时间戳,表示这个任务具体执行时间
    private long time;

    public MyTimerTask(Runnable runnable,long delay){
        this.runnable=runnable;
        this.time=System.currentTimeMillis()+delay;
    }

    public void run(){
        runnable.run();
    }
    public long getTime(){
        return time;
    }
}

注意:

1、通过runnable 成员变量来存储执行的任务

2、通过成员变量 time 来存储任务执行的时间。这里用毫秒时间戳来表示,方便程序进行判断

3、构造方法中的delay表示定时器所定时长,并将time赋值为系统当前毫秒时间戳与delay之和,从而让程序能更精准地确定任务所应执行的时刻。

2、管理任务

存储在定时器中的各个任务一定是执行时间越小越早执行。因此我们每次都会取出任务管理中 time 值最小的任务,将其与当前时间戳进行比较,如果小于等于当前时间戳则开始执行。

这样我们就是不断地在取出当前管理的时间戳最小的任务,显然用 优先级队列 来存储会比较合适。优先级队列就能保证每次取出的元素都是最小(大)的,且取出元素的效率比较高,因此我们可以初步实现一个MyTimer类来通过优先级队列来管理任务MyTimerTask:

class MyTimer{
    private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
}

不过,PriorityQueue类会涉及到大小的比较,因此我们需要让MyTimerTask类重写Comparable接口:

8054e9c3f63c476587d10b3b7053ffe0.png

3、执行任务

在MyTimer内中实现不断取出堆顶元素并与当前时间戳比较,若当前时间戳以大于等于设定时间则执行,否则则不执行:

b7f9eb4367084d19af09e0c4d9584d31.png

再写一个schedule方法来负责往堆中加入新的元素:

f8e52774fb424df398ba3ac573efbadb.png

注意:由于这里涉及了大量的修改操作,为了避免线程安全问题,所以都为这两个操作加上了锁。对于线程安全问题的详细研究可以看一下博主的 深入剖析线程安全问题 一文

不过上述代码其实是存在很大问题的,比如:

(1)当队列中没有元素时:

b00a851d430a491d9c38da5b147450b4.png

该处的逻辑就会在短时间内进行大量地循环,会导致浪费许多系统资源来完成这种无意义的循环。类似于“线程饿死”的情况。

这时我们可以让线程在发现队列为空时就进入 wait 状态,等到调用schedule方法往队列中插入新元素时在调用notify让线程恢复运行即可。关于 wait 与notify的深入研究可以看看博主的 线程的等待与通知机制 。

d33fbd523f6b499f84b8688fa76f7996.png

a8071186f8ad4e66832615129a53dea4.png

(2)队列中已存在元素,但执行时间最小的任务与当前时间仍有一定时间间隔

ad804d9492a24bd7b6b90f327676d55c.png

比如当前时间为9:00,任务时间为11:00,这两个小时内程序就会不断进行上图的循环,也会浪费很多资源。同样的,我们也可以通过wait来解决这一问题:

222679fff45442c2951dc4bf3d718bd5.png

这里的wait与前面不同,不会指望schedule来唤醒。而是应该设置等待时间为任务执行时间与当前时间的差值,这样能保证刚好等待到最小执行任务需要执行的时间,然后重新恢复执行。

注意:这里的wait千万不能换成sleep

1、在等待的过程中,可能会调用 schedule 插入了一个执行时间更小的任务,如果是wait的话,就会被唤醒,重新设定等待时间。而sleep则会死等,中途插入新的任务可能就无法及时执行了

2、sleep休眠时不会释放锁,期间调用schedule方法就无法拿到锁,也就没有办法添加新的元素了

完整代码:

class MyTimerTask implements Comparable<MyTimerTask>{
    private Runnable runnable;
    //通过毫秒时间戳,表示这个任务具体执行时间
    private long time;

    public MyTimerTask(Runnable runnable,long delay){
        this.runnable=runnable;
        this.time=System.currentTimeMillis()+delay;
    }

    public void run(){
        runnable.run();
    }
    public long getTime(){
        return time;
    }
    @Override
    public int compareTo(MyTimerTask o) {
        //此处决定了是大堆还是小堆
        return (int) (this.time-o.time);
    }

}

class MyTimer{
    private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
    public static Object lock=new Object();
    public MyTimer(){
        // 创建线程, 负责执行上述队列中的内容
        Thread t=new Thread(()->{
            while (true){
                synchronized (lock){
                    while(queue.isEmpty()){
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    MyTimerTask current=queue.peek();
                    if(System.currentTimeMillis()>=current.getTime()){
                        //执行任务
                        current.run();
                        //把执行过的任务从队列中删除
                        queue.poll();
                    }else {
                        //先不执行任务
                        try {
                            lock.wait(current.getTime()-System.currentTimeMillis());
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }
        });
        t.start();
    }
    public void schedule(Runnable runnable,long delay){
        synchronized (lock){
            MyTimerTask myTimerTask=new MyTimerTask(runnable,delay);
            queue.offer(myTimerTask);
            lock.notify();
        }
    }

}

三、java标准库中的定时器

标准库中提供了⼀个 Timer 类,可以用来调度定时任务
基本用法:
1、创建Timer实例:
Timer timer = new Timer();

2、创建TimerTask实例:

TimerTask 是一个抽象类,用户需要继承这个类并重写其中的 run() 方法来定义任务的具体行为。

class MyTask extends TimerTask {
    @Override
    public void run() {
        // 在这里写入任务代码
    }
}

3、调度任务:

  • 使用 schedule(TimerTask task, long delay) 方法来安排一个任务在指定的延迟后运行一次。
  • 使用 schedule(TimerTask task, long delay, long period) 方法来安排一个任务在首次延迟后运行,并且之后每隔一个固定的时间间隔重复执行。
Timer timer = new Timer();
MyTask myTask = new MyTask();
timer.schedule(myTask, 1000); // 延迟1秒后执行一次
timer.schedule(myTask, 1000, 60000); // 延迟1秒后开始,每分钟执行一次

4、取消任务:

如果不再需要执行某个任务,可以通过调用 TimerTaskcancel() 方法来取消它

myTask.cancel(); // 取消任务

5、停止Timer

如果要停止整个定时器,可以调用 Timercancel() 方法。

timer.cancel(); // 停止定时器

注意:

  • Timer 和 TimerTask 并不是线程安全的,如果多个线程同时访问同一个 Timer 或者 TimerTask 实例,可能会导致未定义的行为。
  • Timer 创建的线程默认是非守护线程,这意味着如果应用的其他所有非守护线程都结束了,JVM不会自动退出。如果你希望你的应用程序能够在所有工作线程结束后立即退出,你应该考虑将 Timer 设置为守护线程。
  • Timer 是单线程的,所以如果你有多个任务并且其中一个任务执行时间过长,那么它会阻塞其他的任务。

那么本篇文章就到此为止了,如果觉得这篇文章对你有帮助的话,可以点一下关注和点赞来支持作者哦。作者还是一个萌新,如果有什么讲的不对的地方欢迎在评论区指出,希望能够和你们一起进步✊

49b32cfb995a4f01a40b20f0b8a53579.png

  • 36
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 31
    评论
评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值