定时器介绍与使用
先简单介绍一下什么是定时器:定时器类似生活中的闹钟,当时间一到,我们就会去做某些事情。
在代码层面理解就是,当我们设置的时间一到,程序就会执行我们固定的代码片段(也就是任务)
在Java 标准库中给我们提供了定时器的类 Timer
下面是构造方法:
Timer() 就是直接创建一个定时器,里面的属性都是默认值
Timer(boolean isDaemon) 设置定时器里的线程是否为守护线程(后台线程)
Timer(String name) 给你的定时器设置一个名字
schedule
schedule 方法是 Timer 的核心方法,这里介绍第一个schedule 方法的参数含义。
首先 TimerTask task 是 要执行的任务,delay 是时间设置,单位是毫秒(ms)
意思就是在多少毫秒之后就会执行哪些任务。
TimerTask 其实就是对 Runnable 的进一步封装
因此我们在传递task 的时候,要重写 run 方法
代码演示:
public class Demo3 {public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello");}}, 5000);}
}
程序启动之后,5s 之后就会打印 hello
模拟实现
定时器需要定时执行一些任务,这些任务按照各自的时间执行,因此我们可以使用优先级队列来管理这些任务。
为了便于我们实现任务类(MyTimerTask)这里就不采用抽象类的写法,直接定义两个成员变量,一个是 Runnable 任务,一个是 delay 时间,并且在 MyTimerTask 这里实现 Comparable 接口,当然你也可以自己实现一个 Comparator
class MyTimerTask implements Comparable<MyTimerTask>{private Runnable task;private long delay;public MyTimerTask(Runnable task, long delay) {this.task = task;this.delay = delay;}@Overridepublic int compareTo(MyTimerTask o) {return (int)(this.delay - o.delay);}public Runnable getTask() {return task;}public long getDelay() {return delay;}
}
在模拟实现定时器类中,先定义一个优先级队列。
接着我们来写schedule 方法,首先schedule 需要传入两个参数,在Java 源码里是直接传入 task 对象,这里简单一点,直接传入 Runnable ,然后再传入一个 delay 延时时间。
在这个方法中实例化我们的 MyTimerTask 对象,注意时间我们不能直接传过去,我们需要处理一下时间,把 delay + System.currentTimeMillis() 作为时间传入,System.currentTimeMillis() 是系统此时的时间,加上 delay 就是这个任务要在哪个时刻开始执行。
为了避免因为多个线程同时传入任务而导致线程安全问题,这里我们进行加锁,为什么要使用 notify ,后面会提到。
public synchronized void schedule(Runnable task, long delay) {MyTimerTask timerTask = new MyTimerTask(task, System.currentTimeMillis() + delay);queue.offer(timerTask);this.notify();}
最后就是在定时器里创建一个线程,这里我直接在定时器实例化的时候把线程创建出来。
线程的任务就是不断从队列里获取任务然后去执行,因此当队列没有元素的时候,我们需要进行线程等待状态,什么时候结束等待呢?就是当队列添加了一个元素之后,就发出 notify 信号结束等待,所以上面的 schedule 方法最后一行代码是 this.notify() 就是为了提醒这里要结束等待。
结束等待之后,我们就要获取堆顶元素,这里先不着急 poll(),因为我们还不确定这个任务是否是现在就立刻执行,所以我们先 peek(),接着进行判断,如果 task.getDelay() <= System.currentTimeMillis() 的时候就执行任务,否则就是 task.getDelay() > System.currentTimeMillis()
那就进入线程等待,此时我们不能采取死等策略,因为这时候可能没人会发出 notify() 来唤醒这个线程,除了 schedule 方法里 有 notify 之外,可是 schedule 方法的 notify 是起到因为 队列添加了元素,队列不为空唤醒上一个 wait 的作用,因此如果这时候没有人 schedule 的话,这个线程就永远都不会被唤醒了。
因此这里采取有时间的 wait ,this.wait(task.getDelay() - System.currentTimeMillis());
,可是我们知道这个 wait 是有可能因为 schedule 唤醒的,这时候后面的代码不能直接写 任务执行,万一此时队列添加了一个更早的元素的话,按道理你执行的任务就应该更新,**并且此时时间一定没到,wait 提前被唤醒,也是不能执行任务的,**所以这里我们采取 if - else
代码块来写这里的代码,当线程确实被唤醒之后,没事,再走一遍上面的流程重新获取堆顶元素即可。
public MyTimer() {Thread t = new Thread(() -> {try {while(true) {synchronized (this) {while (queue.isEmpty()) {this.wait();}MyTimerTask task = queue.peek();if(task.getDelay() > System.currentTimeMillis()) {this.wait(task.getDelay() - System.currentTimeMillis());} else {task.getTask().run();queue.poll();}}}} catch (InterruptedException e) {throw new RuntimeException(e);}});t.start();}
最终定时器模拟实现的代码:
class MyTimerTask implements Comparable<MyTimerTask>{private Runnable task;private long delay;public MyTimerTask(Runnable task, long delay) {this.task = task;this.delay = delay;}@Overridepublic int compareTo(MyTimerTask o) {return (int)(this.delay - o.delay);}public Runnable getTask() {return task;}public long getDelay() {return delay;}
}class MyTimer {private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();public MyTimer() {Thread t = new Thread(() -> {try {while(true) {synchronized (this) {while (queue.isEmpty()) {this.wait();}MyTimerTask task = queue.peek();if(task.getDelay() > System.currentTimeMillis()) {this.wait(task.getDelay() - System.currentTimeMillis());} else {task.getTask().run();queue.poll();}}}} catch (InterruptedException e) {throw new RuntimeException(e);}});t.start();}public synchronized void schedule(Runnable task, long delay) {MyTimerTask timerTask = new MyTimerTask(task, System.currentTimeMillis() + delay);queue.offer(timerTask);this.notify();}
}