目录
前言
什么是定时器
如何使用java中的定时器
实现计时器
实现MyTimeTask类
Time类中存储任务的数据结构
实现Timer中的schedule方法
实现MyTimer中的构造方法
处理构造方法中出现的线程安全问题
完整代码
考虑在限时等待wait中能否用sleep替换
能否用PriorityBlockingQueue进行存储
在前面,已经讲解了几种常见设计模式,那么今天我们就来讲解一下定时器。
前言
在发送信息的时候,有时候不想要信息那么快就发送出去,而是在特定的时间再发送;或者我们在发送邮件时,当达到特定的时间时,就会自动发送电子邮件给用户,那么这里就需要用到定时器,那么定时器是什么呢?
什么是定时器
定时器是软件开发中的一个重要组件,类似于“闹钟”,能够在某个特定的时间执行一个或者多个任务,定时器是多线程中的一个案例,也是一个比较复杂且重要的案例。
如何使用java中的定时器
在java中,给我们提供了实现了的定时器包,我们可以直接使用。
java中给我们提供的定时器是Timer,我们在设置定时任务时,需要用到其中的schedule方法。
schedule包含两个参数:
第⼀个参数指定即将要执⾏的任务代码
第⼆个参数指定多⻓时间之后 执⾏(单位为毫秒)
示例:
class Demos{public static void main(String[] args) {// 创建一个Timer对象,用于调度定时任务Timer timer=new Timer();// 调度第一个定时任务,1秒后执行timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello");}},1000);// 调度第二个定时任务,2秒后执行timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello");}},2000);// 调度第三个定时任务,3秒后执行timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello");}},3000);}}
实现计时器
我们从上述代码中可以看出,要实现一个定时器,需要实现以下:
- 实现一个任务(Task)类
- 实现一个Timer类用来存放任务
实现MyTimeTask类
/*** TimeTask类用于封装一个延迟执行的任务* 它包含一个需要执行的任务和一个延迟时间*/
class TimeTask{//需要执行的任务private Runnable runnable;//任务等待执行的时间点,以毫秒为单位private long time;/*** 构造函数,用于创建一个TimeTask对象* @param runnable 需要延迟执行的任务,类型为Runnable* @param delay 任务延迟执行的时间,单位为毫秒*/public TimeTask(Runnable runnable,long delay){this.runnable=runnable;//计算任务应该执行的时间点this.time=System.currentTimeMillis()+delay;}/*** 返回任务的等待执行时间* @return 任务等待执行的时间点,以毫秒为单位*/public long getTime(){return this.time;}/*** 执行当前任务* 调用构造时传入的Runnable对象的run方法来执行任务*/public void run(){this.runnable.run();}
}
Time类中存储任务的数据结构
我们在存储任务的时候,需要根据等待时间来存储,时间短的优先取出来,那么我们就可以使用优先级队列,创建一个小根堆,时间最短的放在堆顶。
private PriorityQueue<MyTimeTask> pq=new PriorityQueue<>();
但是我们这里要怎么比较呢?我们可以实现Comparable接口重写compareTo方法来进行比较,或者创建一个类来实现Comparator接口重写compare方法来进行比较。
实现Timer中的schedule方法
当我们解决了在任务在优先级队列中如何进行比较存储任务的问题之后,那么就可以在MyTimer中来实现schedule方法。根据schedule方法的参数创建一个MyTimeTask类,并将其添加到优先级队列中。
/*** 将一个 Runnable 任务安排在指定的延迟时间后执行* * @param runnable 要执行的任务* @param delay 相对于现在的时间延迟,单位为毫秒* * 注意:这个方法使用一个优先队列(pq)来管理这些被安排的任务,确保它们在指定的延迟后被执行*/
public void schedule(Runnable runnable, long delay) {// 创建一个 MyTimeTask 对象,它包含了 Runnable 任务和延迟时间MyTimeTask myTimeTask = new MyTimeTask(runnable, delay);// 将 MyTimeTask 对象添加到优先队列 pq 中,以便在未来的某个时间执行pq.add(myTimeTask);
}
实现MyTimer中的构造方法
通过实例化一个线程,在这个线程中,通过多次扫描优先级队列中的元素,判断堆顶元素是否到达了等待时长,若是,则取出并执行。
注意:这里不能直接poll取出栈顶元素,若栈顶的任务等待时间还未到达,则继续循环。
/*** 构造函数,初始化MyTimer对象* 创建并启动一个线程,用于持续检查并执行已到达设定时间的任务*/public MyTimer(){// 创建一个新的线程来执行定时任务Thread t=new Thread(()->{// 无限循环,持续检查任务队列while(true) {if(pq.isEmpty()){continue;}// 查看在栈顶的任务MyTimeTask task = pq.peek();// 比较栈顶的任务与当前时间点的比较if (System.currentTimeMillis() >= task.getTime()) {// 当前时间已达到任务设定时间,移除任务并执行pq.poll();task.run();}else {// 如果栈顶的任务时间大于当前时间,则继续循环// 继续检查下一个任务,或在没有到达时间的任务时继续等待continue;}}});// 启动线程t.start();}
处理构造方法中出现的线程安全问题
在上述代码中,能看出哪里存在线程安全问题吗?
优先级队列并不是一个线程安全的队列,我们在瞥(peek)取(poll)的时候,可能会出现线程安全问题,若是在多线程环境中,当线程1刚peek了堆顶任务,但此时切换到线程2,线程2同样peek堆顶任务,并刚好到了等待时间,此时就会执行并且删除栈顶任务。此时又切换到线程1,但此时线程1peek的堆顶任务已经被poll掉了,此时如果再执行,就会再次删除堆顶任务,导致出现线程安全问题。
所以,这里我们需要对peek和poll操作进行加锁。
/*** 构造函数,初始化MyTimer对象* 创建并启动一个线程,用于持续检查并执行已到达设定时间的任务*/public MyTimer(){// 创建一个新的线程来执行定时任务Thread t=new Thread(()->{// 无限循环,持续检查任务队列while(true) {synchronized (lock) {// 如果任务队列为空,则跳过当前循环if (pq.isEmpty()) {continue;}// 查看在栈顶的任务MyTimeTask task = pq.peek();// 比较栈顶的任务与当前时间点的比较if (System.currentTimeMillis() >= task.getTime()) {// 当前时间已达到任务设定时间,移除任务并执行pq.poll();task.run();} else {// 如果栈顶的任务时间大于当前时间,则继续循环// 继续检查下一个任务,或在没有到达时间的任务时继续等待continue;}}}});// 启动线程t.start();}
这里还有什么能优化的吗?
我们可以看到,当优先级队列中不为空,但此时堆顶任务的等待时间还没到,此时就会进入else分支执行continue,但一直重复这样操作,可能会造成不断检查,cpu使用率过高。那么我们就可以使用带参数的wait来进行限时等待,当达到时限时,会自动唤醒线程。
同理的,在判断队列是否为空时,我们可以设置不带参数的wait,等待唤醒。
/*** 构造函数,初始化MyTimer对象* 创建并启动一个线程,用于持续检查并执行已到达设定时间的任务*/public MyTimer(){// 创建一个新的线程来执行定时任务Thread t=new Thread(()->{// 无限循环,持续检查任务队列while(true) {synchronized (lock) {// 如果任务队列为空,则跳过当前循环while (pq.isEmpty()) {try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}// 查看在栈顶的任务MyTimeTask task = pq.peek();// 比较栈顶的任务与当前时间点的比较if (System.currentTimeMillis() >= task.getTime()) {// 当前时间已达到任务设定时间,移除任务并执行pq.poll();task.run();} else {// 如果栈顶的任务时间大于当前时间,则继续循环try {lock.wait(task.getTime()-System.currentTimeMillis());} catch (InterruptedException e) {throw new RuntimeException(e);}}}}});// 启动线程t.start();}
既然这里等待,那么我们就需要有人来唤醒wait,所以我们在schedule方法中也需要进行加锁,并且在添加完任务后,调用notify来进行通知。
/*** 将一个 Runnable 任务安排在指定的延迟时间后执行** @param runnable 要执行的任务* @param delay 相对于现在的时间延迟,单位为毫秒** 注意:这个方法使用一个优先队列(pq)来管理这些被安排的任务,确保它们在指定的延迟后被执行*/public void schedule(Runnable runnable, long delay) {synchronized (lock) {// 创建一个 MyTimeTask 对象,它包含了 Runnable 任务和延迟时间MyTimeTask myTimeTask = new MyTimeTask(runnable, delay);// 将 MyTimeTask 对象添加到优先队列 pq 中,以便在未来的某个时间执行pq.add(myTimeTask);// 唤醒可能在等待执行任务的线程lock.notify();}}
MyTimer优化到这里,其实已经优化好了。
完整代码
package Threads;import java.util.Comparator;
import java.util.PriorityQueue;/*** TimeTask类用于封装一个延迟执行的任务* 它包含一个需要执行的任务和一个延迟时间*/
class TimeTask implements Comparable<TimeTask>{//需要执行的任务private Runnable runnable;//任务等待执行的时间点,以毫秒为单位private long time;/*** 构造函数,用于创建一个TimeTask对象* @param runnable 需要延迟执行的任务,类型为Runnable* @param delay 任务延迟执行的时间,单位为毫秒*/public TimeTask(Runnable runnable,long delay){this.runnable=runnable;//计算任务应该执行的时间点this.time=System.currentTimeMillis()+delay;}/*** 返回任务的等待执行时间* @return 任务等待执行的时间点,以毫秒为单位*/public long getTime(){return this.time;}/*** 执行当前任务* 调用构造时传入的Runnable对象的run方法来执行任务*/public void run(){this.runnable.run();}@Overridepublic int compareTo(TimeTask o) {return (int) (this.time- o.getTime());}
}
class MyTimer{//private PriorityQueue<MyTimeTask> pq=new PriorityQueue<>(new compareTimeTask());private PriorityQueue<MyTimeTask> pq=new PriorityQueue<>();static Object lock=new Object();/*** 构造函数,初始化MyTimer对象* 创建并启动一个线程,用于持续检查并执行已到达设定时间的任务*/public MyTimer(){// 创建一个新的线程来执行定时任务Thread t=new Thread(()->{// 无限循环,持续检查任务队列while(true) {synchronized (lock) {// 如果任务队列为空,则跳过当前循环while (pq.isEmpty()) {try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}// 查看在栈顶的任务MyTimeTask task = pq.peek();// 比较栈顶的任务与当前时间点的比较if (System.currentTimeMillis() >= task.getTime()) {// 当前时间已达到任务设定时间,移除任务并执行pq.poll();task.run();} else {// 如果栈顶的任务时间大于当前时间,则继续循环try {lock.wait(task.getTime()-System.currentTimeMillis());} catch (InterruptedException e) {throw new RuntimeException(e);}}}}});// 启动线程t.start();}/*** 将一个 Runnable 任务安排在指定的延迟时间后执行** @param runnable 要执行的任务* @param delay 相对于现在的时间延迟,单位为毫秒** 注意:这个方法使用一个优先队列(pq)来管理这些被安排的任务,确保它们在指定的延迟后被执行*/public void schedule(Runnable runnable, long delay) {synchronized (lock) {// 创建一个 MyTimeTask 对象,它包含了 Runnable 任务和延迟时间MyTimeTask myTimeTask = new MyTimeTask(runnable, delay);// 将 MyTimeTask 对象添加到优先队列 pq 中,以便在未来的某个时间执行pq.add(myTimeTask);// 唤醒可能在等待执行任务的线程lock.notify();}}}
class Demo{public static void main(String[] args) {MyTimer timer=new MyTimer();timer.schedule(()->{System.out.println("Hello World1");},1000);timer.schedule(()->{System.out.println("Hello World2");},2000);timer.schedule(()->{System.out.println("Hello World3");},3000);}
}
class compareTimeTask implements Comparator<MyTimeTask>{@Overridepublic int compare(MyTimeTask o1, MyTimeTask o2) {return (int) (o1.getTime()-o2.getTime());}
}
测试一下
考虑在限时等待wait中能否用sleep替换
Thread.sleep(task.getTime()-System.currentTimeMillis());
这里为什么不用sleep呢?
在前面线程安全问题中已经讲解了wait和sleep的区别,在这里,如果我们使用sleep,会导致拉着锁一起进入睡眠,导致其他线程拿不到锁对象,无法进行加锁。
这会导致我们想要调用schedule方法添加任务时,拿不到锁对象。
能否用PriorityBlockingQueue进行存储
在前面,我们用的是优先级队列PriorityBlockingQueue来存储任务,但如果我们用PriorityBlockingQueue呢?
如果我们使用PriorityBlockingQueue,那么我们的方法也需要改成take(取)和put(存),才能用阻塞等待的效果。
修改代码:
package Threads;import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.concurrent.PriorityBlockingQueue;/*** TimeTask类用于封装一个延迟执行的任务* 它包含一个需要执行的任务和一个延迟时间*/
class TimeTask implements Comparable<TimeTask>{//需要执行的任务private Runnable runnable;//任务等待执行的时间点,以毫秒为单位private long time;/*** 构造函数,用于创建一个TimeTask对象* @param runnable 需要延迟执行的任务,类型为Runnable* @param delay 任务延迟执行的时间,单位为毫秒*/public TimeTask(Runnable runnable,long delay){this.runnable=runnable;//计算任务应该执行的时间点this.time=System.currentTimeMillis()+delay;}/*** 返回任务的等待执行时间* @return 任务等待执行的时间点,以毫秒为单位*/public long getTime(){return this.time;}/*** 执行当前任务* 调用构造时传入的Runnable对象的run方法来执行任务*/public void run(){this.runnable.run();}@Overridepublic int compareTo(TimeTask o) {return (int) (this.time- o.getTime());}
}
class MyTimer{//private PriorityQueue<MyTimeTask> pq=new PriorityQueue<>(new compareTimeTask());private PriorityBlockingQueue<MyTimeTask> pq=new PriorityBlockingQueue<>(100);static Object lock=new Object();/*** 构造函数,初始化MyTimer对象* 创建并启动一个线程,用于持续检查并执行已到达设定时间的任务*/public MyTimer(){// 创建一个新的线程来执行定时任务Thread t=new Thread(()->{// 无限循环,持续检查任务队列while(true) {synchronized (lock) {// 如果任务队列为空,则跳过当前循环while (pq.isEmpty()) {try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}// 查看在栈顶的任务MyTimeTask task = null;try {task = pq.take();} catch (InterruptedException e) {throw new RuntimeException(e);}// 比较栈顶的任务与当前时间点的比较if (System.currentTimeMillis() >= task.getTime()) {// 当前时间已达到任务设定时间,移除任务并执行task.run();} else {// 如果栈顶的任务时间大于当前时间,则继续循环pq.put(task);try {lock.wait(task.getTime()-System.currentTimeMillis());} catch (InterruptedException e) {throw new RuntimeException(e);}}}}});// 启动线程t.start();}/*** 将一个 Runnable 任务安排在指定的延迟时间后执行** @param runnable 要执行的任务* @param delay 相对于现在的时间延迟,单位为毫秒** 注意:这个方法使用一个优先队列(pq)来管理这些被安排的任务,确保它们在指定的延迟后被执行*/public void schedule(Runnable runnable, long delay) {synchronized (lock) {// 创建一个 MyTimeTask 对象,它包含了 Runnable 任务和延迟时间MyTimeTask myTimeTask = new MyTimeTask(runnable, delay);// 将 MyTimeTask 对象添加到优先队列 pq 中,以便在未来的某个时间执行pq.put(myTimeTask);// 唤醒可能在等待执行任务的线程lock.notify();}}}
class Demo{public static void main(String[] args) {MyTimer timer=new MyTimer();timer.schedule(()->{System.out.println("Hello World1");},1000);timer.schedule(()->{System.out.println("Hello World2");},2000);timer.schedule(()->{System.out.println("Hello World3");},3000);}
}
class compareTimeTask implements Comparator<MyTimeTask>{@Overridepublic int compare(MyTimeTask o1, MyTimeTask o2) {return (int) (o1.getTime()-o2.getTime());}
}
由于take会触发阻塞等待,而后面的wait也会,这里加了两次锁,容易引出线程安全问题,所以我们建议使用一个锁对象lock,来进行加锁就行。而不使用无界阻塞队列。
以上就是本篇所有内容~
若有不足,欢迎指正~