关于java标准库中的定时器的使用可以看定时器Timer的使用
大致思路
定义一个MyTimeTask类,该类用于组织要执行任务的内容以及执行任务的时间戳,后面要根据当前系统时间以及执行任务的时间戳进行比较,来判断是否要执行任务或是要等待任务
用一个MyTimer类作为定时器的主体,在MyTimer类中用小根堆这个数据结构来存储要执行的任务
在MyTimer类中定义一个schedule方法,通过传入的参数来实例化一个任务,再将这个任务插入到小根堆中
在MyTimer类中创建一个扫描线程,一方面监控堆首元素是否到点了,一方面在到点后,要调用Runable中的run方法来执行任务,因为在创建MyTimer对象时就应该开始进行对任务的扫描,所以,扫描线程应该定义在MyTimer类的构造方法中
为什么要用小根堆来存储待执行的任务呢?
因为小根堆的堆顶是等待时间最短的任务,在众多任务中,我们只需要关注最早要进行的那个任务就可以了,要是最早要进行的任务都还不能执行,其他任务肯定都不能执行,如果我们不用小根堆的话,用链表的话,我们想要知道接下来要执行的任务就需要一直不停的遍历链表中的任务,将执行任务的时间戳与当前时间进行比较,一直不停的遍历代表一直占用cpu资源,这是不合理的。所以用小根堆来存储待执行的任务呢是比较合理的
MyTimeTask类的创建
代码展示
//描述要执行任务的内容,以及执行任务的时间戳
class MyTimeTask implements Comparable<MyTimeTask>{
//由于该类的对象要存放到小根堆中,所以要实现Comparable接口,重写compareTo方法
// 才可以给出合适的比较规则,才可以放入堆中
@Override
public int compareTo(MyTimeTask o) {
return (int)(this.time-o.time);
}
//要执行任务的内容
private Runnable runnable;
//执行任务的时间戳
private long time;
//构造函数传入要执行的任务以及等待的时间
//delay是一个时间差,类似于3000这样的数值
public MyTimeTask(Runnable runnable,long delay){
this.runnable=runnable;
time=System.currentTimeMillis()+delay; //当前系统时间加上延迟的时间就是任务要执行的时间戳
}
public Runnable getRunnable(){
return runnable;
}
public long getTime(){
return time;
}
}
注意事项
1.由于MyTimeTask类中的time成员属性代表的是任务执行的时间戳,但是用户输入的参数delay是任务执行的延迟时间,所以任务执行的时间戳是当前系统时间加上延迟的时间
2.由于MyTimeTask类要添加到小根堆中,所以要先在MyTimeTask类中定义好放到堆中的优先级关系,这里采用MyTimeTask类实现Comparable接口,重写compareTo方法这个方法来规定MyTimeTask类的优先级关系
MyTimer类的创建
代码展示
/定时器类的主体
class MyTimer{
//用小根堆这个数据结构来存储要执行的任务
//因为小根堆堆顶的数是延迟时间最短,最快要执行的任务
PriorityQueue<MyTimeTask> queue=new PriorityQueue<>();
Object locker=new Object(); //用来加锁的锁对象
//定时器的核心方法,将任务添加到堆中
//涉及到多线程,可能有多个线程调用schedule添加任务到堆中,而且MyTimer内部的线程也要对堆进行修改
//所以存在线程安全问题,并且schedule和扫描线程应该对同一个对象进行加锁
public void schedule(Runnable runnable,long delay){
synchronized(locker){
MyTimeTask task=new MyTimeTask(runnable,delay);
queue.offer(task);
locker.notify();
}
}
//定义一个扫描线程,一方面监控堆首元素是否到点了,一方面在到点后,要调用Runable中的run方法来执行任务
//扫描线程不应该是用户自己调用的,而是一创建MyTimer对象就要有扫描线程来判断是否有需要执行的任务,所以应该写在构造方法中
public MyTimer(){
Thread t=new Thread(()->{
synchronized(locker){ //因为接下来的代码中都涉及到对堆的操作,所以直接整体加上锁
while (true){
//wait推荐和while搭配使用,因为wait可能是不是被notify正常唤醒的,所以被唤醒了以后条件可能依然不满足
// 所以要用while循环对条件是否满足进行多次判断
while (queue.isEmpty()){
//continue;
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//取出任务判断是否需要执行,不删除
MyTimeTask task=queue.peek();
if(task.getTime()<=System.currentTimeMillis()){
task.getRunnable().run(); //时间到了要执行任务
queue.poll(); //执行任务后将任务从堆中弹出
}else {
//任务时间还没有到不需要执行,进行睡眠,等时间到了再执行
try {
//sleep不适合在这里用于进行休眠
//1.sleep休眠不会释放锁,就代表在进行休眠的时候虽然线程没有对堆进行操作
// 但locker对象还是被锁着的,此时要往队列中添加数据也要进行等待,这是不合理的
//2.如果我们新增了一个任务,可能这条任务的执行时间比当前堆顶最近的执行时间还要早进行
//此时我们就需要从睡眠状态脱离,去检查是否要更改随眠时间,但sleep睡眠的过程中不方便提前中断
//虽然可以用interrupt提前中断,但是使用interrupt意味着程序已经要结束了
//所以使用wait来进行休眠是比sleep更加合理的
//Thread.sleep(task.getTime()-System.currentTimeMillis());
locker.wait(task.getTime()-System.currentTimeMillis());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
//线程定义完毕,启动线程
t.start();
}
}
注意事项
1.定义一个schedule方法,用户通过传入任务执行内容以及延迟时间来使用该方法,在schedule中,通过用户传入的参数来实例化一个MyTimeTask的任务,将该任务添加到小根堆中。
2.要进行执行任务的话要在MyTimer内部的扫描线程中执行,由于MyTimer类实例化以后就需要判断是否有新的任务需要处理,所以扫描线程应该创建在MyTimer类的构造方法中,通过循环扫描堆顶的的任务,当堆为空的时候就进入wait睡眠直到用户调用schedule方法向堆中传入任务的时候才调用notify解除睡眠,关于wait和notify可以看通过wait和notify来协调线程执行顺序
当堆不为空的时候,就要取出堆顶的任务根据执行任务的时间戳以及当期系统时间来判断是否需要执行,不删除,要是当前系统时间以及>=任务时间的话,就需要调用task.getRunnable().run()来执行任务,要是当前系统时间以及<任务时间就需要进行wait睡眠,睡眠的时间便是任务执行的时间戳-当前系统时间,因为当前所扫描的这个任务便是最早执行的了,所以其他任务没有扫描的必要,它们更完执行
3.为什么要schedule方法和扫描线程加上synchronized锁呢(关于synchronized锁可以看线程安全问题),因为schedule方法涉及到向堆中添加数据,要是多个线程调用schedule方法就会出现线程安全问题,而扫描线程中也有多次涉及到堆的使用,所以也同样具有线程安全问题,所以要加上synchronized锁来保证线程安全,并且因为schedule方法和扫描线程都是对堆进行修改,所以它们要对同一个对象加锁。
4.sleep同样也可以1进行睡眠,为什么我们要用wait,notify呢
(1).sleep休眠不会释放锁,就代表在进行休眠的时候虽然线程没有对堆进行操作, 但locker对象还是被锁着的,此时要往队列中添加数据也要进行等待,这是不合理的
(2).如果我们新增了一个任务,可能这条任务的执行时间比当前堆顶最近的执行时间还要早进行,此时我们就需要从睡眠状态脱离,去检查是否要更改随眠时间,但sleep睡眠的过程中不方便提前中断,虽然可以用interrupt提前中断,但是使用interrupt意味着程序已经要结束了
所以使用wait来进行休眠是比sleep更加合理的