定时器
定时器如其字面意思,可以让我们的程序在指定时间做操作,例如定时开放活动、定时发送消息等等都可以通过定时器来完成。
标准库的定时器
Java定时器在Java的Timer类中,当我们有定时任务时只需要调用Timer的schedule方法即可:
schedule(TimerTask task, long delay);
//TimerTask是任务类,其构造方法为:TimerTask(Runnable)
使用实例:
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("第一重关");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("第二重关");
}
},4000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("第三重关");
}
},5000);
System.out.println("计时开始");
结果就是在打印“计时开始”3s后打印“第一重关”,1s后“第二重关”,1s后“第三重关”。
实现一个定时器
任务的传递和存储
在实现一个定时器之前我们得先构造出框架,首先我们需要一个MyTimer类作为定时器,在创建一个MyTask类作为任务类。MyTimer类中需要实现schedule(TimerTask task, long delay)方法。当schedule之后,任务就要抛给MyTimer中的容器里,那么谁适合作为这个容器呢?这个容器需要满足能够在短时间内提供当前需要时间最短任务的需求,很明显优先级队列是最佳选择,其次由于定时器往往在多线程中,我们需要使用实现了优先级队列的BlockingQueue阻塞队列来存储数据。
代码实现:
class MyTask implements Comparable<MyTask>{
private Runnable runnable;
private long time;
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
this.time = delay + System.currentTimeMillis();//将相对时间变为绝对时间,便于后面操作
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTask o) {//保证优先级队列出来的任务是时间最短的
return (int)(this.time - o.time);
}
}
public class MyTimer {
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
void schedule(Runnable runnable, long delay) throws InterruptedException {
MyTask task = new MyTask(runnable,delay);
queue.put(task);
}
}
任务的调度
刚刚我们实现了任务的传递与储存,现在我们需要实现任务的调度。我们可以在MyTimer中内置一个线程,让它时刻监控阻塞队列,如果发现任务需要执行就把它执行。这个线程我们可以直接放在构造方法中。
代码实现:
MyTimer(){
Thread t = new Thread(()->{
while (true){
try {
MyTask task = queue.take();
if(task.getTime() > System.currentTimeMillis()){//如果发现最前面的任务还没到时间就把它放回去,否则直接执行
queue.put(task);
}
else {
task.getRunnable().run();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
至此,我们简单的定时器就实现完毕了,但是持续的while循环意味着持续占用CPU资源,那么我们是否有办法对其进行优化呢?
任务调度的优化
在阻塞队列没有任务时我们必须一直循环,保证第一时间拿到任务,但是当阻塞队列有任务时,我们只要等待最早任务需要的时间即可,在这段时间内可以把CPU资源让出来,能实现这个操作的有wait和sleep。但是如果我们使用了sleep,如果在等待的时间内又有线程传递任务过来,我们就无法接收了,如果用wait其它线程可以用notify操作提前唤醒定时器,这才是我们需要的。
public class MyTimer {
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
Object locker = new Object();
MyTimer(){
Thread t = new Thread(()->{
while (true){
try {
MyTask task = queue.take();
if(task.getTime() > System.currentTimeMillis()) {
queue.put(task);
long diff = System.currentTimeMillis() - task.getTime();
synchronized (locker){
locker.wait(diff);//休眠操作
}
}
else {
task.getRunnable().run();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
void schedule(Runnable runnable, long delay) throws InterruptedException {
MyTask task = new MyTask(runnable,delay);
queue.put(task);
locker.notify();//唤醒操作
}
}
这样我们就实现了定时器的调度优化。但是对于多线程,这里的程序还有些问题,如果当循环刚取出优先级队列中的元素时有另一个线程插入任务,即使这个任务要早于刚刚的任务也无法进行调度了,如下图所示:
为了避免这种情况,我们就需要对监控操作进行加锁,保证代码能执行完wait操作再让任务插入。
完整代码如下:
public class MyTimer {
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
Object locker = new Object();
MyTimer(){
Thread t = new Thread(()->{
while (true) {
synchronized (locker) {
try {
MyTask task = queue.take();
if (task.getTime() > System.currentTimeMillis()) {
queue.put(task);
long diff = task.getTime() - System.currentTimeMillis();
locker.wait(diff);
} else {
task.getRunnable().run();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t.start();
}
void schedule(Runnable runnable, long delay) throws InterruptedException {
MyTask task = new MyTask(runnable,delay);
queue.put(task);
synchronized (locker){
locker.notify();
}
}
}
至此,我们的定时器就大功告成了!