定时器是一种能够指定任务在多久时间后执行的组件。
Java标准库里的定时器
在Java标准库中,提供了一个定时器类 Timer,Timer的核心方法为schedule。
schedule包含两个参数,一个是要执行的代码,另一个是多久时间后执行(ms)。
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 3000");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 1000");
}
},1000);
System.out.println("hello main");
}
可以观察到,运行代码后,立刻打印出了hello main等待一秒和三秒后打印出了hello 1000和hello 3000,并且此时 代码没有结束运行.
因为在timer创建后,timer的内部有一个扫描线程,负责扫描schedule进来的代码和时间,等待时间到后这个线程会负责执行代码内容。但是timer不知道后续是否还会schedule进来内容,所以这个线程会一直扫描,不会停止。
想要主动停止timer就要使用到另一个方法:cancel,使用这个方法后,不论现在是否有代码需要执行,timer都会立刻停止。
实现定时器
Java标准库的计时器使用了两个类Timer和TimerTask,所以我们要实现定时器,也要实现两个类MyTimer和MyTimerTask。
需要准备的内容:
MyTimer中需要有一个线程不断的扫描schedule进来的代码是否需要执行。
想要判断谁的时间先到,可以使用优先级队列,让时间先到的位于队首,就能减少需要扫描的数量,节约性能。
MyTimerTask中,需要能够接收要执行的内容,以及多久后执行,需要实现Comparable接口,才能在优先级队列中进行比较。
初步实现:
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;
}
@Override
public int compareTo(MyTimerTask o) {
return (int)(this.time - o.time);
}
//想外界提供时间
public long getTime(){
return time;
}
//执行要执行的内容
public void run(){
//runnable是传入的要执行的代码,调用Runnable自带的run方法可以启动这个代码的执行
runnable.run();
}
}
public class MyTimer {
//优先级队列,存储要执行的内容
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<MyTimerTask>();
//schedule方法
public void schedule(Runnable runnable, long delay){
//传入要执行的内容和时间
//将这下内容传递给task
MyTimerTask task = new MyTimerTask(runnable, delay);
//将task放入执行队列中
queue.offer(task);
}
//创建线程并不断扫描要执行的内容
public MyTimer(){
Thread work = new Thread(() -> {
//不断进行扫描
while(true){
//判断现在队列中是否有要工作的内容
if(queue.isEmpty()){
//执行相应逻辑
continue;
}
//此时有要执行的内容
MyTimerTask task = queue.peek();
//判断是否到达执行时间
if(System.currentTimeMillis() >= task.getTime()){
//时间到,执行代码
task.run();
//将执行后的任务出队
queue.poll();
}else{
//执行时间未到
continue;
}
}
});
work.start();
}
}
但是,这个代码在多线程中是线程不安全的.
queue.offer和queue.peek、poll会对这同一变量queue同时进行读写操作,容易出现问题,所以我们需要加锁.
有了锁,那么对于队列为空和工作内容时间未到的处理就变简单了:进行wait。
修改后的代码:
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;
}
@Override
public int compareTo(MyTimerTask o) {
return (int)(this.time - o.time);
}
//想外界提供时间
public long getTime(){
return time;
}
//执行要执行的内容
public void run(){
//runnable是传入的要执行的代码,调用Runnable自带的run方法可以启动这个代码的执行
runnable.run();
}
}
public class MyTimer {
//优先级队列,存储要执行的内容
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<MyTimerTask>();
public Object lock = new Object();
//schedule方法
public void schedule(Runnable runnable, long delay){
synchronized (lock){
//传入要执行的内容和时间
//将这下内容传递给task
MyTimerTask task = new MyTimerTask(runnable, delay);
//将task放入执行队列中
queue.offer(task);
lock.notify();
}
}
//创建线程并不断扫描要执行的内容
public MyTimer(){
Thread work = new Thread(() -> {
//不断进行扫描
while(true){
synchronized (lock){
//判断现在队列中是否有要工作的内容
if (queue.isEmpty()) {
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//此时有要执行的内容
MyTimerTask task = queue.peek();
//判断是否到达执行时间
if (System.currentTimeMillis() >= task.getTime()) {
//时间到,执行代码
task.run();
//将执行后的任务出队
queue.poll();
} else {
//等待到达执行时间
try {
lock.wait(task.getTime() - System.currentTimeMillis());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
work.start();
}
}