文章目录
- 定时器
- 介绍
- Java标准库中的定时器
- 定时器的实现
定时器
介绍
除了之前说过的单例模式,阻塞队列,线程池以外,定时器也是我们日常开发中常用的代码~
定时器相当于"闹钟".在现实生活中,当闹钟响时,我们就需要去完成一些事情.
同理,在代码中,也经常需要"闹钟机制".
比如在网络通信中,我们经常需要设定一个"超时时间".
在Java标准库中提供了定时器的实现.
Java标准库中的定时器
在Java中提供了Timer,Timer提供了一个方法,叫做schedule.
使用schedule就可以实现定时器的效果.
schedule后面有两个参数:
- 安排一件什么事,通过TimeerTask来表示.
- 时间(单位毫秒),在多久之后干活.
在使用schedule的时候,第二个参数指定的时间是delay值(多长时间后才执行),但是描述任务的时候,不太建议使用delay表示,最好使用"绝对时间"(时间戳)来表示.
import java.util.Timer;
import java.util.TimerTask;
public class Demo21 {
public static void main(String[] args) {
Timer timer = new Timer();
// schedule后面有两个参数:
// 1.安排一件什么事,通过TimeerTask来表示
// 2.时间(单位毫秒),在多久之后干活
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
},3000);
System.out.println("程序开始运行");
}
}
运行以上代码时,程序会首先打印"程序开始运行",等经过了3000ms后才会打印"hello".
Timer不是只能管理一个任务,而是可以管理多个任务:
import java.util.Timer;
import java.util.TimerTask;
public class Demo21 {
public static void main(String[] args) {
Timer timer = new Timer();
// schedule后面有两个参数:
// 1.安排一件什么事,通过TimeerTask来表示
// 2.时间(单位毫秒),在多久之后干活
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello1");
}
},1000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello2");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello3");
}
},3000);
System.out.println("程序开始运行");
}
}
运行结果:
需要注意的是,在我们指定任务的时候,传入的参数是TimerTask而不是Runnable,实际上TimerTask就是基于Runnable又封装了一层.
定时器的实现
接下来让我们自己实现一个简单的定时器~
要想实现定时器需要做到以下3点:
- 创建类,描述一个要执行的任务是啥(任务的内容,任务的时间)
- 管理多个任务,通过一定的数据结构,把多个任务存起来
- 有专门的线程,来执行这里的任务
代码:
import java.util.*;
class MyTimerTask implements Comparable<MyTimerTask> {
private Runnable runnable;
//此处这里的time,通过毫秒时间戳,来表示这个任务具体啥时候执行
private long time;
public MyTimerTask(Runnable runnable, long delay) {
this.runnable = runnable;
//把当前时间加上delay,得到了一个绝对的时间戳,来告诉系统啥时候执行.
this.time = System.currentTimeMillis() + delay;
}
//真正要执行的逻辑
public void run() {
runnable.run();
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTimerTask o) {
return (int) (this.time - o.time);
}
}
class MyTimer {
// // 使用List保存,并不是一个好的选择,我们在后续执行任务列表中的任务的时候,就需要依次遍历每个元素
// // 等程序执行完毕后,还需要把对应的任务从List中删除掉
// // 可以看到效率并不是很高.
// private List<MyTimerTask> list = new ArrayList<>();
// 我们可以使用堆!!
// 因为堆可以很快的找到"最小值"/"第二小"/"第三小"
// 而我们是按照时间来执行任务的,我们只需要确定所有的任务中,时间最小的任务,判断它是否到时间该执行了即可.
// 时间最小的任务,如果还没到时间,那么其他任务就更不可能到时间了~
// 别忘了重写compareTo方法
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
private Object locker = new Object();
public MyTimer() {
Thread t = new Thread(() -> {
try {
while (true) {
synchronized (locker) {
// 确保队列不为空
while (queue.isEmpty()) {
//为空,阻塞
locker.wait();
}
MyTimerTask current = queue.peek();
// 判断当前时间是否大于等于任务时间
if (System.currentTimeMillis() >= current.getTime()) {
// 要执行任务
current.run();
// 把执行过的任务,从队列中删除
queue.poll();
} else {
// 不执行,使用wait的时候会释放cpu资源,让别人使用
locker.wait(current.getTime()-System.currentTimeMillis());
// 这个地方不能用sleep,因为可能会出现以下情况
// 1.你在sleep 1h15min 的过程中,新来了个更早的任务,比如 11:30 执行
// 但是该线程仍在sleep.
// 如果使用wait,每次新来任务,都会把wait唤醒,从而重新设定等待的时间
// 2. sleep休眠的时候,不会释放锁(抱着锁睡)
// 这意味着其他人就拿不到锁了,这就导致新的任务就添加不进来
// 因此使用wait更好!
}
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t.start();
}
public void schedule(Runnable runnable, long delay) {
synchronized (locker) {
MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
queue.offer(myTimerTask);
// 唤醒
locker.notify();
}
}
}
public class Demo22 {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(() -> {
System.out.println("hello1");
}, 1000);
myTimer.schedule(() -> {
System.out.println("hello2");
}, 2000);
myTimer.schedule(() -> {
System.out.println("hello3");
}, 3000);
}
}
有人可能会问了,为啥不使用PriorityBlockingQueue而是自己加锁?
别急,听我慢慢解释:
MyTimerTask current = queue.peek();
首先看这里,这里的take可能会触发阻塞.(PriorityBlockingQueue中的锁)
locker.wait(current.getTime()-System.currentTimeMillis());
再看这里,这里也可能会触发阻塞.(locker锁)
想一想如果上述的阻塞都触发了,那么代码中的锁就变成两把了.
两把锁就有可能会出现死锁的情况.
想要避免死锁,这就需要我们精心控制这里的加锁顺序.
但是这样的话,代码的复杂程度就又提高了不少.
但是其实这里不使用阻塞队列,通篇代码一把锁就可以解决所有问题.
那为啥还要使用PriorityBlockingQueue呢?.
业界实现定时器,除了优先级队列的方式之外,还有一种经典的实现方式,“时间轮”(也是一个巧妙设计的数据结构)
定时器这个东西,很重要,特别常用,尤其是在后端开发中.与"阻塞队列类似",它也会有专门的服务器,用来在分布式系统中实现定时器这样的效果.
本文到这里就结束了~