定时器是实际开发中常用的组件,例如文章的定时发布,双11的准点抢购活动等。
下面我们来看一下Java标准库中的定时器。
1 2 3 4 5 6 7 8 9
| public static void main(String[] args) { Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello"); } }, 3000); }
|
该定时器会在3秒之后输出“hello”。创建一个定时器需要用到Timer类中的核心方法schedule,该方法内有两个参数,一个表示要执行的任务,一个表示任务在多长时间后执行。
认识了标准库中的定时器后,我们可以自己来模拟实现一个定时器。
首先,描述一个任务。创建一个MyTask类,类中有两个属性:一个是执行的任务,一个是任务执行时间。这两个属性类似于标准库schedule方法内的两个参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class MyTask implements Comparable<MyTask>{ private Runnable runnable; private long time;
public MyTask(Runnable runnable,long dalay){ this.runnable = runnable; this.time=System.currentTimeMillis()+dalay; } public void run(){ runnable.run(); } public long getTime(){ return time; }
@Override public int compareTo(MyTask o) { return (int) (this.time-o.time); } }
|
接下来,组织一个任务类。如何组织任务类呢,我们这里用到优先级阻塞队列。每个任务的执行时间(指的是在多长时间后执行)不同,根据时间大小来排序,进而优先执行队头任务,因此需要优先级队列。
最后,我们还需要一个线程不断的去扫描到了时间的任务,然后执行这个任务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| class MyTimer{ private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>(); private Object locker = new Object(); public void schedule(Runnable runnable,long delay){ MyTask myTask = new MyTask(runnable,delay); queue.put(myTask); synchronized (locker){ locker.notify(); } } public MyTimer(){ Thread t1 = new Thread(()->{ while(true){ try { MyTask task = queue.take(); long time=System.currentTimeMillis(); if(time<task.getTime()){ queue.put(task); synchronized (locker){ locker.wait(task.getTime()-time); } }else{ task.run(); } } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); } }
|
模拟一个定时器,总共分三步。第一步,把冰箱打开描述一个任务,即要执行的任务和任务多长时间后开始执行。第二步,组织一个任务,这里用到了优先级阻塞队列。第三步,利用一个线程扫描任务,执行到时间的任务。
下面有两个问题需要注意。
- 任务类要放进优先级阻塞队列中,优先级阻塞队列根据时间先后进行排序。因此我们的任务类要实现
Comparable<MyTask>
接口,然后重写比较规则。
- 线程扫描任务,会从队头取元素,判断是否到时间了,如果没到,再放回队列。接着继续取元素……如果不加限制,它一直不停的扫描队首元素, 看看是否能执行这个任务,这样会大量消耗CPU。因此我们利用wait来使这个线程等待,时间到后再唤醒。此外当新加入一个任务后我们也需要用notify来唤醒扫描线程,因为可能该任务的时间更小,优先级更高,所以需要重新扫描任务队列。(这也就决定了必须用wait,而不能用sleep,因为sleep不能中途唤醒)