[定时器]
目录
一. 定时器的使用
二. 定时器的实现
我们的日常生活中离不开闹钟, 代码中同样需要闹钟, 代码中的"闹钟"就叫做"定时器". 在java标准库中, 也提供了定时器的实现.
一. 定时器的使用
import java.util.Timer;
import java.util.TimerTask;public class Demo31 {public static void main(String[] args) {Timer timer = new Timer(); //创建一个Timer类的对象timer.schedule(new TimerTask() { // schedule(安排), 指定参数两个TimerTask和delay. TimerTask表示要执行的任务, delay表示在多长时间之后执行该任务@Overridepublic void run() {System.out.println("hello world");}}, 3000);System.out.println("程序开始运行");}
}
java中 用Timer类中schedule来完成定时器的实现. schedule()需要指定两个参数:
(1) TimerTask, 这个参数表示了程序要执行的任务.
(2) delay (延时时间), 这个参数表示程序在多长时间之后执行该任务.
上面代码就实现了一个人非常简单的定时器, 这段代码表示: 先打印"程序开始运行", 等待3000ms之后再执行TimerTask中的任务 (打印"hello world") .
Timer也可以安排多个任务:
import java.util.Timer;
import java.util.TimerTask;public class Demo31 {public static void main(String[] args) {Timer timer = new Timer(); //创建一个Timer类的对象timer.schedule(new TimerTask() {// schedule(安排), 指定参数两个TimerTask和delay. TimerTask表示要执行的任务, delay表示在多长时间之后执行该任务@Overridepublic void run() {System.out.println("hello world 3");}}, 3000);timer.schedule(new TimerTask() { // Timer也可以安排多个任务@Overridepublic void run() {System.out.println("hello world 2");}}, 2000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello world 1");}}, 1000);System.out.println("程序开始运行");}
}
上述代码, 由于hello1 延时1000ms打印, hello2延时2000ms打印, hello3延时3000ms打印, 所以打印顺序应该是hello1 -> hello2 -> hello3
二. 定时器的实现
了解了定时器的基本用法之后, 接下来我们就自己具体实现一下定时器吧~
实现一个定时器, 需要完成以下几个重要的步骤:
(1) 创建类, 描述一个要执行的任务 (任务的内容, 任务执行的时间).
(2) 管理多个任务: 通过一定的数据结构, 把多个任务管理起来.
(3) 指定专门的线程, 执行这里的任务.
import java.util.PriorityQueue;class MyTimerTask implements Comparable<MyTimerTask>{private Runnable runnable;private long time;public MyTimerTask(Runnable runnable, long delay) { //runnable->要执行的任务, delay->延时时间this.runnable = runnable;this.time = System.currentTimeMillis() + delay;// System.currentTimeMillis() --> 当前的时间戳.}public void run() {runnable.run();}public long getTime() {return time;}@Overridepublic int compareTo(MyTimerTask o) {//此处减的顺序就决定了是大堆还是小堆 (我们这里需要小堆)// 谁减谁才能得到小堆? -> 实验(尝试)的方式return (int) (this.time - o.time);}
}
class MyTimer {//private List<MyTimerTask> list = new ArrayList<>();// 这里使用list保存任务不是一个很好的选择. 如果用list保存任务,// 后续我们执行列表中的任务的时候, 就需要依次遍历每个元素, 任务执行完毕之后, 还需要把对应的任务从list中删除掉.private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();// 此时这里的TimerTask还需要指定比较规则.(实现comparable接口 / 指定comparator)public MyTimer() {// 创建线程, 执行上述队列中的内容.Thread t = new Thread(() -> {while (true) {if (queue.isEmpty()) { //如果队列为空, 无法取出任务. continuecontinue;}MyTimerTask current = queue.peek(); //取出队首元素if (System.currentTimeMillis() >= current.getTime()) {//执行任务current.run();//删除执行过的任务queue.poll();} else {//时间没到, 不执行任务continue;}}});t.start();}public void schedule(Runnable runnable, long delay) {MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);queue.offer(myTimerTask);}}
显然, 上述代码没有针对线程安全问题作出处理.
import java.util.PriorityQueue;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;// System.currentTimeMillis() --> 当前的时间戳.}public void run() {runnable.run();}public long getTime() {return time;}@Overridepublic int compareTo(MyTimerTask o) {return (int) (this.time - o.time);}
}
class MyTimer {private Object locker = new Object();public MyTimer() {// 创建线程, 执行上述队列中的内容.Thread t = new Thread(() -> {while (true) {synchronized (locker) {if (queue.isEmpty()) { //如果队列为空, 无法取出任务. continuecontinue;}MyTimerTask current = queue.peek(); //取出队首元素if (System.currentTimeMillis() >= current.getTime()) {//执行任务current.run();//删除执行过的任务queue.poll();} else {//时间没到, 不执行任务continue;}}}});t.start();}public void schedule(Runnable runnable, long delay) {synchronized (locker) {MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);queue.offer(myTimerTask);}}
}
当初始情况下,队列为空时, 经过 if 判定, 就会执行continue, 在执行while循环, 再if判定, 还是空, 再continue, 这样的话, 当前线程就一直在循环做一些没有意义的事情, 而且还占用着锁资源. 所以, 我们这里执行wait()是一个更好的选择. 在这里wait(), 就需要在queue加入元素之后唤醒(notify). 我们在前一篇文章阻塞队列那里是说过, wait()外的循环使用while更好一些, 所以我们这里把if换成while.
假设队列中已经包含元素了, 但是执行时间还没到, 那么 if 判定不满足, 就会执行else, 执行里面的continue, 然后在while(true)进来, 再执行到 if 判定, 仍不满足, 再执行else, 继续continue, 这样一来, 这里就跟上面的那种情况一样了, 在循环做一些没有意义的事情, 而且占用着锁资源. 那么如何解决这样的问题呢? --> 还是使用wait(), 不过这里wait()不用notify唤醒, 而是使用超时时间. 等待了指定时间之后自动唤醒. 注意这里使用wait()而不能使用sleep(), 因为如果在sleep过程中来了一个执行时间更早的任务, 那么sleep不会唤醒, 但是wait()就会被唤醒. 而且sleep在休眠的时候不会释放锁资源, 它是"抱着锁"睡的, 这样的话其他线程想拿锁资源就无法拿到了.
这里我们不使用java提供的BlockingQueue而是自己加锁的原因是: 如果使用BlockingQueue, 那么它的take()方法和wait()带锁, 这里就出现两把锁了, 就很有可能导致死锁问题的出现.
import java.util.PriorityQueue;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;// System.currentTimeMillis() --> 当前的时间戳.}public void run() {runnable.run();}public long getTime() {return time;}@Overridepublic int compareTo(MyTimerTask o) {return (int) (this.time - o.time);}
}
class MyTimer {private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();// 此时这里的TimerTask还需要指定比较规则.(实现comparable接口 / 指定comparator)private Object locker = new Object();public MyTimer() {// 创建线程, 执行上述队列中的内容.Thread t = new Thread(() -> {try{while (true) {synchronized (locker) {while (queue.isEmpty()) { //如果队列为空, 无法取出任务. continue//continue;locker.wait();}MyTimerTask current = queue.peek(); //取出队首元素if (System.currentTimeMillis() >= current.getTime()) {//执行任务current.run();//删除执行过的任务queue.poll();} else {//时间没到, 不执行任务//continue;locker.wait(current.getTime() - System.currentTimeMillis());// 用任务执行时间减去当前时间, 得到一个时间差, wait就等待这么长时间. 如果到点了就唤醒.}}}}catch (InterruptedException e) {throw new RuntimeException(e);}});t.start();}public void schedule(Runnable runnable, long delay) {synchronized (locker) {MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);queue.offer(myTimerTask);locker.notify();}}
}
那么以上就是修改完全之后的最终代码了~
我们可以在main方法中试一下看它是否和系统提供的定时器功能一样:
public class Demo32 {public static void main(String[] args) {MyTimer myTimer = new MyTimer();myTimer.schedule(new Runnable() { // 可以重写run方法指定任务@Overridepublic void run() {System.out.println("hello 3000");}}, 3000);myTimer.schedule(() -> { //也可以使用lambda表达式System.out.println("hello 2000");},2000);myTimer.schedule(()-> {System.out.println("hello 1000");}, 1000);}
}
执行结果没有问题~
定时器除了用优先级队列的方式实现, 还有一种经典的实现方式 -> 时间轮. 这里我们就再不做讨论了.