在软件开发中,有一些代码逻辑并不需要立马就被执行,可能需要等一段时间在执行。就好像我们会用闹钟来提醒我们过一段时间后要做某事一样,代码中也有“定时器”这种类似于闹钟的机制。那么,计时器都有什么特点,又该如何使用呢,就让博主手把手带你研究一下吧。
一、什么是定时器
常见作用: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接口:
3、执行任务
在MyTimer内中实现不断取出堆顶元素并与当前时间戳比较,若当前时间戳以大于等于设定时间则执行,否则则不执行:
再写一个schedule方法来负责往堆中加入新的元素:
注意:由于这里涉及了大量的修改操作,为了避免线程安全问题,所以都为这两个操作加上了锁。对于线程安全问题的详细研究可以看一下博主的 深入剖析线程安全问题 一文
不过上述代码其实是存在很大问题的,比如:
(1)当队列中没有元素时:
该处的逻辑就会在短时间内进行大量地循环,会导致浪费许多系统资源来完成这种无意义的循环。类似于“线程饿死”的情况。
这时我们可以让线程在发现队列为空时就进入 wait 状态,等到调用schedule方法往队列中插入新元素时在调用notify让线程恢复运行即可。关于 wait 与notify的深入研究可以看看博主的 线程的等待与通知机制 。
(2)队列中已存在元素,但执行时间最小的任务与当前时间仍有一定时间间隔
比如当前时间为9:00,任务时间为11:00,这两个小时内程序就会不断进行上图的循环,也会浪费很多资源。同样的,我们也可以通过wait来解决这一问题:
这里的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 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、取消任务:
如果不再需要执行某个任务,可以通过调用 TimerTask
的 cancel()
方法来取消它
myTask.cancel(); // 取消任务
5、停止Timer
如果要停止整个定时器,可以调用 Timer
的 cancel()
方法。
timer.cancel(); // 停止定时器
注意:
Timer
和TimerTask
并不是线程安全的,如果多个线程同时访问同一个Timer
或者TimerTask
实例,可能会导致未定义的行为。Timer
创建的线程默认是非守护线程,这意味着如果应用的其他所有非守护线程都结束了,JVM不会自动退出。如果你希望你的应用程序能够在所有工作线程结束后立即退出,你应该考虑将Timer
设置为守护线程。Timer
是单线程的,所以如果你有多个任务并且其中一个任务执行时间过长,那么它会阻塞其他的任务。
那么本篇文章就到此为止了,如果觉得这篇文章对你有帮助的话,可以点一下关注和点赞来支持作者哦。作者还是一个萌新,如果有什么讲的不对的地方欢迎在评论区指出,希望能够和你们一起进步✊