目录
1. 标准库中的定时器
1.1 Timer 的定义
- Java 的Timer是一个用于调度任务的工具类,用于在未来某个时刻执行任务或周期性地执行任务。
- Timer类一般与 TimerTask 搭配使用,其中 TimerTask 是一个需要执行的任务。
- 适用于简单的定时任务,如定时更新、定期发送报告等。
1.2 Timer 的原理
Timer可以实现延时任务,也可以实现周期性任务,它的核心就是一个优先队列和封装的执行任务的线程。
实现原理
- 维持一个小顶堆,即最快需要执行的任务排在优先队列的第一个,根据堆的特性我们知道插入和删除的时间复杂度都是O(log n)。
- 然后有个TimerThread线程,不断地拿排着的 第一个任务 的执行时间,和当前时间做对比。
- 如果时间到了先看看这个任务是不是周期性执行的任务:
- 如果是周期性任务,则修改当前任务时间为下次执行的时间;
- 如果不是周期性任务,则将任务从优先队列中移除,最后执行任务。
- 如果时间还未到则调用wait() 等待。
可以看出Timer,实际就是根据任务的执行时间,维护了一个优先队列,并且起了一个线程,不断地拉取任务执行。
1.3 Timer 的使用
编译器一般都是Runnable 来描述任务,但是在定时器这里稍微特殊一点,把 Runnable 封装成了 TimerTask;TimeTask 本质上是一个抽象类:
示例一
程序运行结果
进程一直没有结束,是因为 Timer 和线程池一样 ,都包含了前台线程,阻止进程结束;
1.4 Timer 的弊端
- 首先优先队列的插入和删除的时间复杂度是O(logn),当数据量大的时候,频繁的入堆出堆性能有待考虑。
- 并且是单线程执行,那么如果一个任务执行的时间过久,则会影响下一个任务的执行时间(当然我们设置任务的 run(),要是异步执行也行)。
- 并且从它对异常没有做什么处理,所以一个任务出错的时候会导致之后的任务都无法执行。
1.5 ScheduledExecutorService
- ScheduledExecutorService 是 Java5 引入的替代方案,功能更强大。
- 它支持多线程并行调度任务,能更好地处理任务调度的复杂场景。
- 因为使用线程池进行任务调度,所以不会因某个任务的异常终止,而导致其他任务停止。
- 并且它提供了更灵活的API,可以更精细地控制任务的执行周期和策略。
- 推荐使用ScheduledExecutorService 替代Timer。
示例二
注意事项
程序运行结果
2. 模拟实现定时器
2.1 实现定时器的步骤
2.1.1 定义类描述任务
定义类描述任务
通过 MyTimerTask类 来描述,在定时器定时的时间结束后,线程要执行的任务 :
第一种定义方法
基于抽象类的方式定义MyTimerTask,并实现 Runnable 接口,并且重写 run();
这样的定义虽然确实可以,写起来有点麻烦,还有另外的写法;
第二种定义方法
可以不把 MyTimerTask类 设置成抽象类,而是在MyTimerTask类的成员中,持有一个 Runnable:
后续通过构造方法参数,把定义的任务传进来:
上述两种写法都是可以的;
定义 MyTimerTask 类的时间属性
MyTimerTask 不但要描述要执行的任务,还要记录什么时候任务被执行:
并提供相应的方法来获取任务执行时间,和任务执行方法:
完善描述任务类的比较规则
- 因为要通过优先级队列比较任务的执行时间,对于要比较的元素是 int,String 这种本身就有明确比较规则的对象,可以不额外指定;
- 但是我们自己定义的类 MyTimerTask 是没有明确比较规则的,所以我们需要给 MyTimerTask 实现比较规则,实现 Comparable 接口,重写 compareTo() 方法;
- 这个优先级队列是小根堆,让时间少的任务先执行,如果分不清楚建立的是小根堆还是大根堆,就排列组合,直到找到合适的计算表达式;
- time 之间的计算结果是 long 类型的,compareTo() (是优先级队列内部调用的),返回类型是 int,所以需要强转;
2.1.2 使用优先级队列实现 Timer
- 定时器的构成是一个优先级队列(不要使用PriorityBlockingQueue,容易死锁);
- 队列中的每个元素是一个 Task 对象,因此我们自己实现 Timer,可以把泛型参数设置成刚刚实现的,用来描述任务的类 MyTimerTask:
2.1.3 实现 schedule() 方法
对该方法的简单说明
把任务添加到队列中
2.1.4 扫描线程中负责执行队列中的任务
创建 worker
同时有一个worker线程一直扫描队首元素,看队首元素是否需要执行 ;所以通过 MyTimer 的构造方法,来创建 Thread 对象:
task 中带有一个时间属性,队首元素就是即将要执行的任务。
模拟队列为空时的阻塞效果
如果在往队列中取任务的时候,发现队列为空,则模拟出线程被队列阻塞的效果:
只有当任务执行完才可以将其移除出优先级队列,否则只能通过 peek() 取任务;
完善对任务的时间属性进行比较的操作
和线程池不同,线程池是只要队列不为空,就立即取任务并执行;
但是 worker 需要关注队首元素的时间属性,系统时间到了任务执行时间,队首元素才会被取出并且执行,否则时间不到,任务不能执行:
在拿到任务后,我们需要比较当前系统时间是否已经到任务执行时间,没有到就继续通过 continue 的方式模拟阻塞 ,否则执行该任务,并且在执行完后出队列。
处理线程安全问题
当前调用 schedule 是一个线程,定时器内部又有一个线程,多个线程操作同一个队列,一定涉及到线程安全问题,所以我们要给 submit() 和 worker 中的操作加锁:
构造方法本身可以写synchronized,但是这个地方不能这么写,要保护的逻辑是 lambda表达式中的 run() ,run() 和构造方法是两个不同的方法;
安排任务
程序运行结果
- worker 需要关注队首元素的时间属性,系统时间到了任务执行时间,队首元素才会被取出并且执行,否则时间不到,任务不能执行;
- 和线程池不同,线程池是只要队列不为空,就立即取任务并执行;
- worker 在发现队列为空时,会陷入 continue 模拟出来的阻塞等待,进程继续保持运行状态;
2.2 忙等问题
什么是忙等
- 定时器在执行上述任务,会出现忙等问题,忙等并没有实质性地做任何工作,只是在等待。
- 但是忙等又和 sleep 这样的等待不同:
- sleep 这样的等待是会释放 CPU 资源的等待,如果这个线程什么都不干,就不参与CPU资源的调度,把CPU资源让给其他线程;
- 但是忙等 既要消耗 CPU 资源,又不执行任务,这样的设定是不科学的,所以我们就需要针对这样的忙等,进行进一步的优化
出现忙等问题的代码
通过 wait() ,notify() 解决忙等问题
当第一个 wait() 被触发,说明队列为空,只需要调用 schedule() 往队列中添加任务即可;
schedule() 里的操作恰好也需要锁,所以添加wait() notify() (都需要搭配锁来使用)来减轻程序忙等的现象,顺理成章;
因此唤醒第一个 wait() 的 notify() 在 schedule() 中设置;
对于解决两处忙等问题的思路如下
- 第一个wait(),一被唤醒了,下面的逻辑被执行,是和当前 wait() 的判断条件(队列是否为空),是不冲突的;
- 队列为空,只要时间到了,也能执行下面的逻辑,因此需要把 if() 改成 while(),让第一个 wait() 被唤醒后,再次判断是否满足唤醒条件;
- 第二个 wait() 的唤醒条件,只看时间是否到了,到了才能执行下面的逻辑;
- 所以第二个wait被打断了,else也会判断当前系统时间,是否已经到任务执行时间,因此不需要把 if() 改成while
补充
单击其中一个 locker,其他 locker 同时显示,证明获取的 locker 为同一个;
虽然是在 lamda 中的对 this 加锁,看着像是 this 指向匿名内部类,然后对匿名内部类加锁,但是实际上是 lamda 的变量捕获,使得此处的this指向 MyTimer 这个外部类;
2.3 模拟实现定时器完整代码
package Thread;
import java.util.PriorityQueue;
import java.util.TimerTask;
import java.util.concurrent.Executors;
class MyTimerTask implements Comparable<MyTimerTask>{
private Runnable task;
//记录要执行的任务的时刻
private long time;
public MyTimerTask(Runnable task, long time) {
this.task = task;
this.time = time;
}
@Override
public int compareTo(MyTimerTask o) {
return (int)(this.time-o.time);
}
public long getTime(){
return time;
}
public void run(){
task.run();
}
}
//模拟实现一个定时器
class MyTimer{
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
private Object locker = new Object();
public void schedule(Runnable task , long delay){
synchronized (locker){
MyTimerTask timerTask = new MyTimerTask(task,System.currentTimeMillis()+delay);
queue.offer(timerTask);
locker.notify();
}
}
public MyTimer(){
//创建一个线程,负责执行队列中的任务
Thread worker = new Thread(() -> {
try {
while (true){
synchronized (locker){
//取出队首元素
while(queue.isEmpty()){
locker.wait();
}
MyTimerTask task = queue.peek();
if(System.currentTimeMillis()<task.getTime()){
locker.wait(task.getTime() - System.currentTimeMillis());
}else {
task.run();
queue.poll();
}
}
}
}catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"worker");
}
}
public class Demo33 {
public static void main(String[] args) {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 3000");
}
},3000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 2000");
}
},2000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 1000");
}
},1000);
//Executors.newScheduledThreadPool(4);
}
}