8/20 Java学习博客
新的日常开发组件:定时器
作用:约定一个时间,时间到达之后,执行某个代码逻辑
定时器非常常见尤其实进行网络通信的时候
就比如客户端发出请求就要等待响应,但是服务器迟迟没有响应,因为网络环境的复杂,也不知道请求是没发过去,还是响应丢了,或是服务器出问题了,对于客户端来说,不能无限的等,需要有一个最大的期限,到达这个期限后,是重新发一遍还是放弃还是其他什么。
这是这个等待的最大期限,就是通过定时器方式来实现
首先要使用定时器 其实这个使用方法跟创建线程时候差不多吧我感觉
先写一个类继承TimerTask类 然后重写run方法
class TimerTask1 extends TimerTask{
@Override
public void run() {
System.out.println("执行定时器的任务1000");
}
}
这可能是因为TimerTask 他也实现了Runnable接口
这里我就用TimerTask1来继承的
//创建TimeTask1实例
TimerTask1 timerTask1 = new TimerTask1();
//然后创建Timer实例
Timer timer = new Timer();
//给定时器安排任务 不是像线程一样
/* Runnable runnable = new MyRunnable();
Thread t =new Thread(runnable);
t.start();
*/
//他是需要这个schedule 来安排一个任务,预定时间去执行
timer.schedule(timerTask1,1000); //当前安排的任务啥时候执行,以当前时刻为基准往后推迟xxx ms
这样一个定时器的使用就写好了
当然对于这个schedule也是可以使用匿名内部类的写法 继承TimerTask创建实例,重写run方法
timer.schedule(new TimerTask(){//此处使用匿名内部类的写法,继承了TimerTask并且创造出一个实例
@Override
public void run(){//目的重写run方法 通过run描述任务的详细情况
System.out.println("执行定时器的任务2000");
}
},2000);
主线程执行schedule方法时候,就是把任务放到timer对象中了,与此同时,timer里面也包含了一个线程,叫做扫描线程,一旦时间到了,扫描线程就会执行刚才安排的任务了
仔细观察可以发现,全部打印完后,进程依然没有结束,就是因为timer内部线程阻止了进程结束。
我们不光要学怎么用 还要学怎么实现简单定时器
首先分析下定时器中所需
1.需要有一个线程,来扫描任务是否到时间,可以执行了。
2.需要有一个数据结构,把所有的任务都保存起来。
3.还需要创建个类,通过类的对象来描述一个任务(至少包含任务内容和时间)
所以我们先创建类 通过这个类描述对象,然后在里面定义一些必要字段,然后写一个构造方法
class MyTimerTask{
private long time;//执行时间的时间
private Runnable runnable;
public MyTimerTask(Runnable runnable,long delay){
this.runnable=runnable;
this.time=System.currentTimeMillis()+delay;
}
//此处的delay是相对时间,其实就是我们在util里面的timer.schedule(timertask,delay)里设置的多少ms后执行的delay
public long getTime(){
return time;
}
public Runnable getRunnable(){
return runnable;
}
}
一个初步的类就搞好了
接着我们实现下保存对象的数据结构,那优先级队列最好了,因为我们需要每次等待都是等待最新的任务。
接着这里我们写定时器里面还包含了扫描线程
扫描线程实现这3个主要地方:
- 队列为空时,阻塞等待新任务加入
2.队列不为空,查看队首任务
3.时间到了,弹出并执行任务,时间没到进行等待
然后我们就要队列空实现阻塞 加锁 有阻塞还要有唤醒
所以我们在添加任务的schedule方法上用lock.notify()
队列不为空 则就先查看队首任务
定时器主体应该这样写
//自己的定时器
class MyTimer{
//使用一个数据结构 优先级队列,保存所有要安排的任务
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<MyTimerTask>();
//使用这个对象作为锁对象
private Object lock = new Object();
//通过schedule方法把要完成的任务,构造成个任务对象放入 队列里面
//对于优先级队列,要求里面的元素务必可比较。 还有TreeSet TreeMap
public void schedule(Runnable runnable,long delay){
synchronized (lock){
queue.offer(new MyTimerTask(runnable,delay));
lock.notify();
}
}
//搞个扫描线程
public MyTimer(){
//创建一个扫描线程
Thread t = new Thread(()->{
while (true) {
try {
synchronized (lock) {
// 1. 队列为空时,阻塞等待新任务加入
while (queue.isEmpty()) {
lock.wait();
}
// 2. 队列不为空,查看队首任务
MyTimerTask task = queue.peek();
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()) {
// 时间到了,弹出并执行任务
task = queue.poll();
task.getRunnable().run();
} else {
// 时间还没到,有限期等待对应的差值时间
// 在此期间如果有更新的时间更早的任务被 schedule 入队,notify() 会提前唤醒它
lock.wait(task.getTime() - curTime);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
/* 这里回忆下 初次开始的错误写法
逻辑结构严重错位(核心卡死原因)
现象:当队列为空时,线程执行到 lock.wait() 进入阻塞。即使此时主线程通过 schedule() 插入了任务并调用 lock.notify() 唤醒它,线程唤醒后会继续从 wait() 下一行执行。
问题:在我最初的代码里,queue.peek() 和执行任务的逻辑全写在 catch 代码块之前(紧接着 wait() 后面)。一旦通过 lock.wait() 挂起,它只会在被唤醒的那一次去执行 peek()。后续如果不继续手动 notify,或者队列不为空时,它甚至根本进不去 while (queue.isEmpty()) 循环,导致后续任务完全无法处理!
//需要不停扫描队首元素 是否到达时间
while(true){
synchronized (lock){
//取队首元素,前提是队列是否为空,空的话就是阻塞等待
while(queue.isEmpty()){
//使用wait进行等待,但是要需要搭配synchronize加锁
try {
lock.wait();
//这里的wait()很明显需要由添加新的任务的线程唤醒
MyTimerTask task = queue.peek();
long curTime = System.currentTimeMillis();
if(curTime >= task.getTime()){//当前时间到达任务时间,可以执行任务
task.getRunnable().run();
//任务执行完了
queue.poll();
}else{
//时间没到,暂时啥都不干,等待下一轮判定
}
//比较下当前队首元素是否可以执行了
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});*/
t.start();
}
}
说到队首任务 就不得不提到我们这个 优先级队列
PriorityQueue 会认为:
你没给我 Comparator,那我就把元素当成 Comparable 来比较。
但你的对象又不能比较。
所以你有两种写法都可以。
第一种,重写 compareTo
第二种,不让 MyTimerTask 实现 Comparable,直接创建队列时告诉它怎么排
这里我们选择第一种 实现这个接口并且重写 使得 比较后是队首元素最小即时间最短
因为用到了比较时间所以我们还要在类对象加一个获取时间 getTime
然后我们这里的查看对象是用MyTimerTask task = queue.peek();
获取任务然后执行,还需要getRunnable;
所以 类应该这么写
class MyTimerTask implements Comparable<MyTimerTask>{
//要有一个执行的任务
private Runnable runnable;
//还要一个执行任务得时间
private long time;
//此处得delay就是schedule方法传入的相对时间
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);
//如果是想让队首元素是时间最大的
//return o.time - this.time;
}
/**
* compareTo 返回值约定:
* 返回负数:this 对象排在前面
* 返回0:两个对象相等
* 返回正数:o 对象排在前面
* PriorityQueue 默认是小顶堆,堆顶(队首)放优先级最小的元素。
*/
public long getTime() {
return time;
}
public Runnable getRunnable() {
return runnable;
}
}
最后启动扫描线程start()
这样一个简单的计时器就做好了
我其实写的时候一直在想这个我们实现的计时器为什么schedule是传runnable而不是MyTimerTask
官方 Timer:
外部创建 TimerTask
我的 MyTimer:
内部创建 MyTimerTask
其实我们可以
只需要把 schedule 从:
public void schedule(Runnable runnable, long delay) {
synchronized (lock) {
queue.offer(new MyTimerTask(runnable, delay));
lock.notify();
}
}
改成接收自己的 MyTimerTask:
public void schedule(MyTimerTask task) {
synchronized (lock) {
queue.offer(task);
lock.notify();
}
}
然后外部创建:
MyTimerTask task = new MyTimerTask(
() -> System.out.println("执行任务"),
3000
);
timer.schedule(task);
流程就变成:
外部 new MyTimerTask
↓
timer.schedule(task)
↓
放进 PriorityQueue
↓
到时间执行
不过还是有点不太像吧,毕竟官方的是把delay放在task外面的
总而言之一个简单的计时器就写好了

浙公网安备 33010602011771号