前言👀~
上一章我们介绍了阻塞队列以及生产者消息模式,今天我们来讲讲定时器
定时器
标准库中的定时器
schedule()方法
扫描线程
手动实现定时器
任务类
存储任务的数据结构
定时器类
如果各位对文章的内容感兴趣的话,请点点小赞,关注一手不迷路,讲解的内容我会搭配我的理解用我自己的话去解释如果有什么问题的话,欢迎各位评论纠正 🤞🤞🤞
个人主页:N_0050-CSDN博客
相关专栏:java SE_N_0050的博客-CSDN博客 java数据结构_N_0050的博客-CSDN博客 java EE_N_0050的博客-CSDN博客
定时器
定时器是个非常常见的组件,尤其是在网络进行通信的时候,类似发邮件,类似于一个 "闹钟",达到一个设定的时间之后, 就执行某个指定好的代码
举个例子,当客户端给服务器发送请求后,服务器半天没有响应,就像你发邮件一样,发的时候会转圈圈,成功了就会显示发送成功或者什么提示信息,如果服务器没有响应,你这边可能就一直在那转圈圈。我们也不知道是什么原因造成的,可能是请求没发过去,可能是响应丢了,也可能是服务器出现了问题。所以对于客户端来说,也可以说对用户来说,肯定不能一直等啊那体验多不好啊,所以设置一个等待时间(最大的期限),过了这个等待时间把电脑砸了,开个玩笑,过了这个最大期限,我们选择重新发一遍,或者直接不发,或者重开这个程序等等方式。这里的最大期限我们可以使用定时器去实现
标准库中的定时器
首先我们先使用一下定时器Timer类,再去讲解,代码如下
public class Test1 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("启动成功");
}
}, 1000);
System.out.println("原神启动");
}
}
输出结果
schedule()方法
这个方法涉及两个参数 第一个参数描述了任务要做什么这里使用匿名内部类去创建一个TimerTask实例,第二个参数就是时间就是要在多长时间(单位为毫秒)后去执行,这个时间是根据当前时间为准然后根据你设定的时间来执行任务的,比如说现在11:00:00你设置1秒后执行就是11:00:01执行任务。然后前面用匿名内部类创建出来的TimerTask实例实现了Runnable接口,然后我们重写方法定义自己要执行的任务通过schedule方法,接着再由扫描线程去执行
扫描线程
当我们创建出这个timer对象后,这个线程也就被创建出来了,后续要执行任务,都是通过这个线程去执行的
来看刚才这段代码以及输出结果
public class Test1 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("启动成功");
}
}, 1000);
System.out.println("原神启动");
}
}
输出结果,你会发现整个进程并没有结束,主线程执行schedule方法的时候,是把这个任务丢给timer对象中的一个线程去处理的,这个线程可以叫"扫描线程",你设置的时间一到,就去扫描任务也就是执行你写的任务。
解释:为什么整个进程没有结束?timer中的这个线程阻止了进程结束,它在等我们再给它安排任务,相当于服务员,你有什么吩咐它就执行,没有任务就在那等并且timer里可以安排多个任务
手动实现定时器
根据上面标准库可以得出以下要求:
1.和上面标准库提供的timer类一样,我们需要一个扫描线程,然后去执行任务
2.需要一个数据结构,把所有要执行的任务保存起来
3.需要使用一个类,通过一个类的对象去来描述执行的任务(任务内容和执行时间)
任务类
首先写一个用来描述任务的类,包含任务内容和执行时间
在设置任务执行时间的时候,有两种方式,一种是相对的时间,一种是绝对的时间(完整的时间戳),两种都可以这里我们选择绝对时间,因为相对时间要计算间隔后的时间然后进行比较,绝对时间获取当前时间戳加上任务执行时间然后进行比较。
下面是任务类的实现,不只这一种,最后完整代码有两种
//用来描述任务的类 包含任务的内容和执行时间
class MyTimerTask {
private Runnable runnable;
private long time;
public MyTimerTask(Runnable runnable, long delay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;//使用绝对时机 时间戳+传入的时间
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
}
存储任务的数据结构
这里的数据结构我们采用优先级队列去保存需要执行的任务,因为我们肯定要先执行时间最少的任务,然后优先级队列也就是堆,最顶层的就是最小的,并且优先级队列取出元素(也就是获取时间最少的任务)时间复杂度都为O(1)
public PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
但是注意优先队列要求放入的元素是可以比较的,也就是我们的任务之间可以进行比较,所以我们还需要实现自定义比较器,使用时间进行比较。除了优先级队列中的元素需要能进行比较的,还有二叉搜索树也就是TreeMap和TreeSet
public PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(new Comparator<MyTimerTask>() {
@Override
public int compare(MyTimerTask o1, MyTimerTask o2) {
return (int) (o1.getTime() - o2.getTime());
}
});
定时器类
我们的定时器和标准库中的定时器一样,我们需要一个扫描线程执行任务,还需要一个schedule方法,上面的优先级队列也放在定时器中,下面是代码实现,需要注意线程不安全问题,会出现这样的可能就比如主线程在向队列添加元素的时候,扫描线程也在对队列进行判断,导致加入了元素的时候这里正好进行判断,然后为空进入阻塞状态
class MyTimer {
//优先级队列存储任务 优先级队列的元素要能进行比较 所以要实现比较器 我们根据时间进行比较
public PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(new Comparator<MyTimerTask>() {
@Override
public int compare(MyTimerTask o1, MyTimerTask o2) {
return (int) (o1.getTime() - o2.getTime());
}
});
public MyTimer() {
//创建出定时器对象的时候 启动扫描线程
thread.start();
}
//给用户调用的方法 传入要完成的任务以及时间
public void schedule(Runnable runnable, long delay) {
synchronized (lock) {//避免线程不安全问题 有任务了就唤醒线程进行工作
if (delay < 0) {
throw new IllegalArgumentException("输入的时间有误");
} else {
queue.offer(new MyTimerTask(runnable, delay));//调用这个方法的时候 创建任务然后放进队列进行处理
lock.notify();
}
}
}
public Object lock = new Object();
public Thread thread = new Thread(() -> {
synchronized (lock) {
while (true) {//即使没任务 也等我们给它分配任务
while (queue.isEmpty()) {//队列为空进入阻塞 使用while保险起见
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
long currentTime = System.currentTimeMillis();//记录当前时间
MyTimerTask task = queue.peek();//先看任务的时间 如果到了再poll
if (currentTime >= task.getTime()) {
queue.poll();
task.getRunnable().run();//获取到引用去执行用户的任务
} else {
}
}
}
});
}
还有一个地方需要进行优化比如就是你设置执行任务的时间在10点半,然后else那块不写代码,它会一直到while循环开始判断一路下路,一直到时间到去执行任务,这样做消耗太多cpu资源,解决办法,让线程在这里休息,使用带参数的wait方法,当前执行任务时间减去当前时间作为参数
public Thread thread = new Thread(() -> {
synchronized (lock) {
while (true) {//即使没任务 也等我们给它分配任务
while (queue.isEmpty()) {//队列为空进入阻塞 使用while保险起见
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
long currentTime = System.currentTimeMillis();//记录当前时间
MyTimerTask task = queue.peek();//先看任务的时间 如果到了再poll
if (currentTime >= task.getTime()) {
queue.poll();
task.getRunnable().run();//获取到引用去执行用户的任务
} else {
}
}
}
});
两种完整代码
第一种任务类是没有直接实现Runnable接口
//用来描述任务的类 包含任务的内容和执行时间
class MyTimerTask {
private Runnable runnable;
private long time;
public MyTimerTask(Runnable runnable, long delay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;//使用绝对时机 时间戳+传入的时间
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
}
//定时器
class MyTimer {
//优先级队列存储任务 优先级队列的元素要能进行比较 所以要实现比较器 我们根据时间进行比较
public PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(new Comparator<MyTimerTask>() {
@Override
public int compare(MyTimerTask o1, MyTimerTask o2) {
return (int) (o1.getTime() - o2.getTime());
}
});
public MyTimer() {
//创建出定时器对象的时候 启动扫描线程
thread.start();
}
//给用户调用的方法 传入要完成的任务以及时间
public void schedule(Runnable runnable, long delay) {
synchronized (lock) {//避免线程不安全问题 有任务了就唤醒线程进行工作
if (delay < 0) {
throw new IllegalArgumentException("输入的时间有误");
} else {
queue.offer(new MyTimerTask(runnable, delay));//调用这个方法的时候 创建任务然后放进队列进行处理
lock.notify();
}
}
}
public Object lock = new Object();
public Thread thread = new Thread(() -> {
synchronized (lock) {
while (true) {//即使没任务 也等我们给它分配任务
while (queue.isEmpty()) {//队列为空进入阻塞 使用while保险起见
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
long currentTime = System.currentTimeMillis();//记录当前时间
MyTimerTask task = queue.peek();//先看任务的时间 如果到了再poll
if (currentTime >= task.getTime()) {
queue.poll();
task.getRunnable().run();//获取到引用去执行用户的任务
} else {
}
}
}
});
}
第二种是任务类实现Runnable接口
//用来描述任务的类 就是存储任务的内容以及执行时间
class MyTimerTask implements Runnable {
private long time;
private Runnable task;
public MyTimerTask(Runnable runnable, long delay) {
this.task = runnable;
this.time = System.currentTimeMillis() + delay;//使用绝对时间 当前时间戳+多少秒后执行=执行时间
}
public long getTime() {
return time;
}
@Override
public void run() {//外层的这个就是一个壳,通过调用这个方法执行里面我们自己写的任务
task.run();// 这个就是我们自己写的任务
}
}
//定时器 包含存储队列 扫描线程 创建任务
class MyTimer {
public Object lock = new Object();
//使用优先级队列存储任务 因为取出任务的时间复杂度为0(1) 注意要比较器 因为我们要使用时间比较出谁是最小的
public PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(new Comparator<MyTimerTask>() {
@Override
public int compare(MyTimerTask o1, MyTimerTask o2) {
return (int) (o1.getTime() - o2.getTime());
//return Long.compare(o1.getTime(), o2.getTime());//可以避免溢出
}
});
//初始化定时器就启动扫描线程
public MyTimer() {
thread.start();
}
//把任务和执行时间传到这方法 然后通过这个方法创建任务类去装任务和时间
public void schedule(Runnable runnable, long delay) {
synchronized (lock) {
if (delay < 0) {
throw new IllegalArgumentException("输入的时间有误!!!");
} else {
queue.offer(new MyTimerTask(runnable, delay));
lock.notify();
}
}
}
//创建扫描线程执行任务
public Thread thread = new Thread(() -> {
//因为扫描线程会一直扫描任务 它在等我们再给它安排任务
while (true) {
synchronized (lock) {
while (queue.isEmpty()) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//不是直接poll 任务执行的时候要和当前时间进行比较后 再进行poll去执行
//这个拿的任务相当于我们自己写的任务
MyTimerTask task = queue.peek();
long currentTime = System.currentTimeMillis();
//如果当前时间等于或者超过任务的执行时间就执行任务
if (currentTime >= task.getTime()) {
task.run();//
queue.poll();
} else {
try {
//让线程休息到执行任务的时间
lock.wait(task.getTime() - currentTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
}
以上便是本章内容,定时器在日常开发中还是会用到的,例如发邮件这类的,所以还是需要好好掌握,我们下一章再见💕