目录
🍂代码
🍂执行结果
🍂设计思路
🍂代码
🍂思考
🍂运行结果
标准库提供的方法
定时器,顾名思义,我们可以设置一个任务,要求在多长时间时候执行。可以布置好几个任务,哪个任务的时间到了执行哪一个。
我们可以使用标准库中的Timer类来实现定时器方法。
代码
public class TestTimer {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("这是第一个任务");
}
},1000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("这是第二个任务");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("这是第三个任务");
}
},3000);
}
}
这里面 TimerTask实际上也就是一个Runnable,会重写其中的run方法。
执行结果
可以看到当三个任务执行结束之后程序并没有退出,而是阻塞等待。
自己实现一个定时器
设计思路
根据标准库中的代码,我们自己实现定时器时也可以使用这种思路,首先定时器既然要达到定时的效果,就要判断添加到任务执行的时间顺序,所以我们先确定要使用一个带有优先级并且有阻塞功能的队列来存放每个任务,所以我们就可以直接使用PriorityBlockingQueue。其次我们设计的Timer类中也可以使用schedule方法来添加任务,在构造方法里创建线程进行任务的具体执行。
参考标准库方法,我们可将具体任务抽象出来一个类(Timertask)。那么类的属性就有执行的时间time和Runnable等属性。 这里因为我们需要将添加进来的任务进行一个比较排序,所以time需要使用绝对的时间戳加上任务设计规定的时间。
我们的任务存在PriorityBlockingQueue中,也就是存的每个元素是一个可比较的Timertask,所以Timertask需要实现Comparable接口,重写compareTo方法。
其他实现的具体细节我们边写代码边分析。
代码
Timertask
class Timertask implements Comparable<Timertask>{
//时间使用绝对的时间戳
public long time;
public Runnable runnable;
public Timertask(Runnable runnable,long time){
this.runnable = runnable;
//绝对时间戳加上相对时间time得到的时间就是当前任务要执行的实际时间
this.time = System.currentTimeMillis()+time;
}
@Override
public int compareTo(Timertask o) {
return (int) (this.time-o.time);
}
}
这个类来表示一个具体要进行的任务。
Timer
public class MyTimer {
//带有优先级的阻塞队列
PriorityBlockingQueue<Timertask> blockingQueue = new PriorityBlockingQueue<>();
//创建锁对象
Object clock = new Object();
public void schedule(Runnable runnable,long time){
//这里面的time参数填的是形如1000,2000的相对时间,表示当前时间的基础上过多久在执行任务
//构造一个任务,并添加进队列
Timertask timertask = new Timertask(runnable,time);
blockingQueue.put(timertask);
synchronized (clock){
clock.notify();
}
}
//创建线程去执行任务
public MyTimer(){
Thread t = new Thread(()->{
while (true){
try {
synchronized (clock) {
Timertask ts = blockingQueue.take();
//当前的时间
long curtime = System.currentTimeMillis();
if (curtime >= ts.time) {
//到了执行任务的时间
ts.runnable.run();
} else {
//时间还没到, 将取出的任务又重新放回队列中
blockingQueue.put(ts);
//为了避免忙等
clock.wait(ts.time - curtime);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//别忘了启动线程
t.start();
}
代码的整体思路与我们上面的思路差不多,也有几处细节问题需要思考一些
思考
这两边的代码只是加锁的地方不同,思考这两种会有什么区别
左边的加锁是正确的,而右边的加锁则会造成两个问题:1.notify会造成空打一炮的问题,也就是还没有wait阻塞等待,就先notify解等待了。2.会错过一些任务没有执行到。
造成这一问题的罪魁祸首还是因为线程的随机调度和无序抢占式执行。
运行结果
class Main{
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 3");
}
},3000);
myTimer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 2");
}
},2000);
myTimer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 1");
}
},1000);
System.out.println("hello 0");
}
}