定时器
- 什么是定时器
- 标准库中的定时器
- 使用
- 定时器的实现
什么是定时器
定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.
标准库中的定时器
标准库中提供了一个Timer类, java.util.Timer
使用
Timer 类的核心方法为 schedule()
.
schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).
Timer timer = new Timer();
timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("3000");}
}, 3000);
这种写法很像Runnable的写法, 实际上Time类实现了Runnable接口
上述代码的意思就是在3000ms之后, 执行run()方法. 但run()方法不是在调用schedule()方法的那个线程中执行的, 而是在Timer内部的线程中执行的. 并且为了保证随时可以处理新安排的任务, 这个线程会持续执行, 并且该线程是前台线程, 此线程不结束, 那么该进程就不会结束.
定时器的实现
一个定时器里是可以有多个任务的
MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("3");}
}, 3000);
timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("2");}
}, 2000);
timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("1");}
}, 1000);
先要能够把一个任务描述出来, 然后再用合适的数据结构将多个任务组织起来
步骤:
-
创建一个
TimerTask
这样的类, 表示一个任务, 这个任务就需要包含两方面: 任务的内容, 任务的实际执行时间.实际执行时间可以用时间戳表示, 在调用schedule()的时候, 先获取到当前的系统时间, 在这个基础上, 加上delay时间间隔, 得到了真实要执行这个任务的时间
-
使用一定的数据结构, 把多个TimerTask组织起来.
-
如果使用List组织TimerTask的话, 如何确定此时该执行那个任务呢? 如果使用一个线程不停的对List进行遍历, 查看该任务是否到达执行时间, 就会很低效.
-
优化: 不需要扫描所有的任务, 我们只需要关注时间最靠前的任务就行, 因为时间最早的任务还没有执行的话, 其他的任务时间也肯定还没有到. 那么如何知道那个任务时间是最靠前的呢? 此时我们就想到了一种数据结构叫做堆/优先级队列.
-
所以使用 堆/优先级队列 来组织这些任务, 就能以最快的速度找到时间最靠前的, 也就是队首元素.
-
针对一个任务的扫描也不必反复的一直执行, 而是在获取到队首元素的时间后, 和当前的系统时间做个差值, 根据这个差值来觉该线程休眠等待的时间, 在到达这个时间之前, 不会进行重复扫描. 这样便大大降低了扫描的次数. 并且提高了资源的利用率, 避免了不必要的cpu浪费.
-
-
提供一个扫描线程: 一方面 负责监控队首元素是否到达执行时间; 另一方面 当任务到达执行时间后, 调用run()执行任务.
-
注意线程安全问题: 主线程和扫描线程都会对队列操作, 要注意线程安全, 需要加锁.
//创建一个类, 描述定时器中的一个任务
class MyTimerTask implements Comparable<MyTimerTask> {//任务的具体执行时间private long time;//具体任务private Runnable runnable;/*** @param runnable 具体执行的任务* @param delay 是一个相对时间差*/public MyTimerTask (Runnable runnable, long delay) {time = System.currentTimeMillis() + delay;this.runnable = runnable;}public long getTime() {return time;}public Runnable getRunnable() {return runnable;}@Overridepublic int compareTo(MyTimerTask o) {return (int)(this.time - o.time);}
}
//定时器类
class MyTimer {//锁对象private Object object = new Object();//使用优先级队列保存任务private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();/*** 定时器的核心方法, 就是把要执行的任务添加到队列中* @param runnable 要执行的任务* @param delay 相对时间差* 主线程操作queue, 需要加锁*/public void schedule(Runnable runnable, long delay) {synchronized (object) {MyTimerTask task = new MyTimerTask(runnable, delay);queue.offer(task);//每次添加新的任务后之前休眠的线程唤醒, 根据最新的任务情况, 更新最新任务的执行时间和线程休眠时间object.notify();}}/*** 扫描线程: * 一方面 负责监控队首元素是否到达执行时间,* 另一方面 当任务到达执行时间后, 调用run()执行任务.* 对queue的操作需要加锁*/public MyTimer() {//扫描线程Thread t = new Thread(() -> {while (true) {try {synchronized (object) {//只要queue是空, 那么线程开始休眠, 直到队列不再为空while (queue.isEmpty()) {object.wait();}//获取队首元素MyTimerTask myTimerTask = queue.peek();//或许当前时间long curTime = System.currentTimeMillis();//将当前时间与任务执行时间做比较, 判断是否执行if (curTime >= myTimerTask.getTime()) {queue.poll();//时间到了, 直接执行myTimerTask.getRunnable().run();} else {//时间不到, 当前线程休眠object.wait(myTimerTask.getTime() - curTime);}}} catch (InterruptedException e) {e.printStackTrace();}}});//线程开始t.start();}
}
object.wait(myTimerTask.getTime() - curTime);
object.wait();
这里为什么不用
sleep()
?
- 因为sleep()休眠的线程不会释放锁, 那么如果扫描线程正在休眠的话, 主线程调用schedule就会出现阻塞. 也不能因为线程在休眠而不能添加新的任务了吧~
- sleep在休眠的过程中, 不方便被提前唤醒. 因为每次添加新的任务都要把之前休眠的线程唤醒, 并且根据最新的任务情况, 更新最新任务的执行时间.