- 博主简介:努力的打工人一枚
- 博主主页:@xyk:
- 所属专栏: JavaEE初阶
定时器是什么?定时器能做什么?本篇文章带你深入了解并实现
目录
一、定时器
1.1java标准库定时器
1.2定时器代码
二、模拟定时器实现
2.1先创建个任务类,表示执行的任务是啥,任务啥时候执行?
2.2创建定时器类
三、完整代码及注意事项
一、定时器
定时器是什么?
定时器也是软件开发中的一个重要组件. 类似于一个 "闹钟". 达到一个设定的时间之后, 就执行某个指定好的代码
定时器是一种实际开发中非常常用的组件,比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连,比如一个 Map, 希望里面的某个 key 在 3s 之后过期,类似于这样的场景就需要用到定时器
标准库中提供了一个 Timer 类,Timer 类的核心方法为 schedule;
schedule 包含两个参数,第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒)安排一个工作,这个工作不是立即完成的,而是未来某个时间点~~
public class ThreadDemo6 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
},4000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
},1000);
}
}
可见线程还没有结束,那这是因为什么呢?
是因为Timer里面内置了线程,(还是前台线程)会阻止线程结束
定时器,内部管理的不仅仅是一个任务,可以管理很多任务的!!
虽然任务有很多,但是他们的触发时间是不同的,每次都找到这些任务中,最先到达的任务执行;一个线程先执行最早的任务,做完了之后再执行第二早的,那么应该用什么去存储这些任务呢?
当然是堆!!!java标准库中提供了带优先级的阻塞队列
- 队列中的每个元素是一个 Task 对象
- Task 中带有一个时间属性, 队首元素就是即将要执行的任务
- 同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行
// 表示一个任务. class MyTask implements Comparable
{ public Runnable runnable; // 为了方便后续判定, 使用绝对的时间戳. public long time; public MyTask(Runnable runnable, long delay) { this.runnable = runnable; // 取当前时刻的时间戳 + delay, 作为该任务实际执行的时间戳 this.time = System.currentTimeMillis() + delay; } @Override public int compareTo(MyTask o) { return (int) (this.time - o.time); } }
class MyTimer{ // 这个结构, 带有优先级的阻塞队列. 核心数据结构 private PriorityBlockingQueue
queue = new PriorityBlockingQueue<>(); // 创建一个锁对象 private Object locker = new Object(); // 此处的 delay 是一个形如 3000 这样的数字 (多长时间之后, 执行该任务) public void schedule(Runnable runnable,long delay){ // 根据参数, 构造 MyTask, 插入队列即可. MyTask myTask = new MyTask(runnable,delay); queue.put(myTask); synchronized (locker){ locker.notify(); } } // 在这里构造线程, 负责执行具体任务了. public MyTimer(){ Thread t = new Thread(() -> { while (true){ try { // 阻塞队列, 只有阻塞的入队列和阻塞的出队列, 没有阻塞的查看队首元素. synchronized (locker) { MyTask myTask = queue.take(); long curTime = System.currentTimeMillis(); if (myTask.time <= curTime) { // 时间到了, 可以执行任务了 myTask.runnable.run(); } else { // 时间还没到 // 把刚才取出的任务, 重新塞回队列中. queue.put(myTask); locker.wait(myTask.time - curTime); } } } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); } }
注意几个点:
1.要让任务类去实现Comparable接口,以至于可以放进PriorityBlockingQueue
2.使用wait去避免忙等,浪费系统资源
3.在放任务的时候,用notify来唤醒线程
/**
* @author xyk的电脑
* @version 1.0
* @description: TODO
* @date 2023/3/25 16:10
*/
public class ThreadDemo1 {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello4");
}
}, 4000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello3");
}
}, 3000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello2");
}
}, 2000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello1");
}
}, 1000);
System.out.println("hello");
}
}
// 表示一个任务.
class MyTask implements Comparable{
public Runnable runnable;
// 为了方便后续判定, 使用绝对的时间戳.
public long time;
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
// 取当前时刻的时间戳 + delay, 作为该任务实际执行的时间戳
this.time = System.currentTimeMillis() + delay;
}
@Override
public int compareTo(MyTask o) {
return (int) (this.time - o.time);
}
}
class MyTimer{
// 这个结构, 带有优先级的阻塞队列. 核心数据结构
private PriorityBlockingQueue queue = new PriorityBlockingQueue<>();
// 创建一个锁对象
private Object locker = new Object();
// 此处的 delay 是一个形如 3000 这样的数字 (多长时间之后, 执行该任务)
public void schedule(Runnable runnable,long delay){
// 根据参数, 构造 MyTask, 插入队列即可.
MyTask myTask = new MyTask(runnable,delay);
queue.put(myTask);
synchronized (locker){
locker.notify();
}
}
// 在这里构造线程, 负责执行具体任务了.
public MyTimer(){
Thread t = new Thread(() -> {
while (true){
try {
// 阻塞队列, 只有阻塞的入队列和阻塞的出队列, 没有阻塞的查看队首元素.
synchronized (locker) {
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis();
if (myTask.time <= curTime) {
// 时间到了, 可以执行任务了
myTask.runnable.run();
} else {
// 时间还没到
// 把刚才取出的任务, 重新塞回队列中.
queue.put(myTask);
locker.wait(myTask.time - curTime);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
注意事项:
1.使用wait来等待,而不是sleep,wait方便随时提前唤醒
2.wait的参数是“超时时间”,时间达到一定程度后,还没有notify就不等,如果时间还没到,就notify立即返回
会导致新进来的最早的任务“空打一炮”,导致新的任务无法及时执行了;关键要点:多线程的调度是随机的,无序的!!!