✨✨hello,愿意点进来的小伙伴们,你们好呐!
🐻🐻系列专栏:【JavaEE】
🐲🐲本篇内容:自己实现Java定时器
🐯🐯作者简介:一名现大二的三非编程小白,日复一日,仍需努力。
1. 什么是定时器
在日常生活中,如果我们想要在 t1 后去做一件重要的事情,那么为了防止忘记,我们就可以使用闹钟的计时器功能,规定了 t1 时间后提醒我们去执行这件事情. — 这就是Java定时器的简单功能
2. Java内置定时器的常用功能
Java中的定时器的类是 : Timer ,为util包中的一个无继承关系的类, 从该类的构造方法中,我们可以使用无参构造器创建该类的对象,也可以在创建类对象的时候指定定时器中所需要的线程的名字,与是否为守护进程
在定时器中最常用的方法就是 schedule(TimerTask task, long delay)
该方法传参的是一个 TimerTask 对象,与定时器约定的执行时间间隔 delay
而 TimerTask 类则是一个来描述计时器任务的类,该类中有 抽象方法 run(),所以我们给 schedule 传参中的 TimerTask 对象都会重写 run() 方法,然而 重写的run() 方法中的语句,则是定时器需要执行的语句.
而 delay 则是我们约定从当前时间后的 delay 内执行传入的任务.时间单位为 毫秒
接下来我们来看一个简单的定时器的使用 :
我们在创建定时器的时候指定了定时器中的扫描线程的线程名,然后使用 schedule 方法传入任务与任务执行的间隔时间 2000 毫秒
这个时候在执行该代码的2000毫秒后,定时器就会将该任务执行
public class TimerTest {
public static void main(String[] args) {
//创建定时器的时候指定定时器中内置线程的名字
Timer timer = new Timer("内置线程");
timer.schedule(new TimerTask() {
@Override
public void run() {
//Thread.currentThread().getName() 执行到该任务的线程名
System.out.println(Thread.currentThread().getName() + " hello");
}
},2000);
}
}
在上述说到有一个定时器内置的扫描线程
这个扫描线程是怎么回事呢?又在定时器中起到什么作用呢?在接下来的自己实现定时器中就会介绍到.
3.自定义定时器
3.1 实现定时器思路
1.实现定时器,我们首先需要有一个可以来描述定时器中的任务的类 MyTimerTask
2.需要使用一个数据结构将定时器中的任务按照执行时间的顺序给组织起来
3.在 MyTimer 定时器类中会有一个线程不断地去访问定时器的任务,查看是否到了指定执行时间.
3.2 MyTimerTask 类:
在MyTimerTask类中,我是这样设计的:
1.实现了Runnable接口与Comparable接口,Comparable是来重新指定比较规则.
2.在类中编写了抽象方法 run(),让创建该类对象的时候可以重新run()方法,用来规划任务内容.
3.nextExecutionTime是来记录当前任务执行的时间是什么时候,并附有nextExecutionTime的set与get方法
import java.util.Comparator;
/**
* @author 罗鸿基
* @version 1.0
* 这个是一个描述线程任务的类
* 该类中需要有
*/
public abstract class MyTimerTask implements Runnable, Comparable<MyTimerTask> {
//使用抽象方法 run 方法来让创建该线程的重写 run 方法
public abstract void run();
//该任务执行的时间
protected long nextExecutionTime;
public MyTimerTask() { }
public long getNextExecutionTime() {
return nextExecutionTime;
}
public void setNextExecutionTime(long nextExecutionTime) {
this.nextExecutionTime = nextExecutionTime;
}
//因为是使用优先级队列,所以就要重写compareTo方法,制定比较规则
@Override
public int compareTo(MyTimerTask o) {
return (int) (this.getNextExecutionTime() - o.getNextExecutionTime());
}
}
3.3 MyTimer 类:
MyTimer 类设计如下 :
1.该类中需要有一个用来组织任务的数据结构,我采用优先级阻塞队列来实现组织任,因为PriorityBlockingQueue 实现了BlockingQueue接口,可以当为线程安全的队列,而我们需要对每一个任务按照执行时间的顺序进行排序.所以我选择PriorityBlockingQueue来实现该功能
2.在该类中,我们需要有一个线程,不断地对任务队列中优先级最高(最快执行)的任务进行查看, 看是否到达执行时间.
3.在schedule方法中,我们需要修改任务中的执行时间,并将任务插入任务队列
package csdn;
import java.util.concurrent.PriorityBlockingQueue;
/**
* @author 罗鸿基
* @version 1.0
* 这个是定时器类
* 需要有一个用来组织任务的数据结构 -- PriorityBlockingQueue
*/
public class MyTimer {
//该队列用来组织任务
//因为有可能有多个线程去访问任务队列,使用就使用PriorityBlockingQueue (阻塞队列)
//因为也需要判断任务的优先级(执行的时间间隔差异),所以我们所以优先级队列
private PriorityBlockingQueue<MyTimerTask> queue = new PriorityBlockingQueue<>();
//扫描线程
private Thread thread;
public MyTimer() {
//该线程用来不断扫描任务队列中是否有可执行的任务
thread = new Thread(() -> {
while (true) {
try {
//取出当前队列中执行时间间隔最短的任务
//如果队列中没有任务,那么就一直等待
MyTimerTask take = queue.take();
//取出任务执行的时间
long nextExecutionTime = take.getNextExecutionTime();
//取出系统当前的时间
long newTime = System.currentTimeMillis();
//两个时间对比,看是否到达执行时间
if (nextExecutionTime > newTime) {
//未到执行时间
//将任务重新放入队列
queue.put(take);
}else {
//到达执行时间,就执行任务
take.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//启动扫描线程
thread.start();
}
public void schedule(MyTimerTask task,long delay) {
//以防别的线程同时往定时器中插入任务
synchronized (queue) {
//修改任务中的时间
task.setNextExecutionTime(System.currentTimeMillis() + delay);
//将任务插入队列
queue.put(task);
}
}
}
执行 :
public class TimerTest {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new MyTimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 延迟1000毫秒的任务 hello");
}
},1000);
myTimer.schedule(new MyTimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 延迟2000毫秒的任务 hello");
}
},2000);
myTimer.schedule(new MyTimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 延迟3000毫秒的任务 hello");
}
},3000);
}
}
执行结果符合我们的预期
我们可以看到当前Java进程中的确有一个线程一直在工作,该线程就是扫描线程
且当前线程是属于等待状态
但是的确的定时器其实是有不小的问题的,我们下面来分析问题并解决
3.4 定时器的问题 :
3.4.1 忙等 :
如果队列中没有任务,那么取出任务会就一直等待,那么就还不会造成忙等,但是如果是队列中有任务的话,当前线程就会不断循环,取出任务来进行查看.这种行为是很浪费CPU资源的,下面我们来进行优化
3.4.2 CPU随机调度导致的bug
对于多线程代码中,CPU的随机调度是万恶之源,会导致很多意想不到的bug出现.
下面我们来举个例子来解释一下CPU随机调度带来的bug :
例如 : 在某一时刻线程线程从队列中取出任务的执行时间为14:00(下图中红色的的线所指向),然后这个时候扫描线程被操作系统调度后不再继续执行
而此时又有一个线程去调用了schedule() 方法,加入一个任务,这个任务的执行时间为13:30.然后加入任务队列,执行notify()
那么这个时候操作系统再继续调度到取出任务后,发现距离任务执行的时间还有一段距离,这个时候就调用wait(time)方法
然后接下来没有任务加入了,就不会调用notify()方法,那么线程就加入等待状态,等待到14.00再唤醒,那么这个时候就会完美地错过了新加入线程的执行时间
图解:
代码实现:
public MyTimer() {
//该线程用来不断扫描任务队列中是否有可执行的任务
thread = new Thread(() -> {
while (true) {
synchronized (lock) {
try {
//取出当前队列中执行时间间隔最短的任务
MyTimerTask take = queue.take();
//取出任务执行的时间
long nextExecutionTime = take.getNextExecutionTime();
//取出系统当前的时间
long newTime = System.currentTimeMillis();
//两个时间对比,看是否到达执行时间
if (nextExecutionTime > newTime) {
//未到执行时间
//将任务重新放入队列
queue.put(take);
lock.wait(nextExecutionTime - newTime);
} else {
//到达执行时间,就执行任务
take.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});