目录
一、定时器是什么?
定时器是一种用于在指定时间间隔或特定时间点执行特定任务的工具或设备。在计算机科学中,定时器通常是软件或硬件组件,用于跟踪时间的流逝并在预定的时间触发事件或执行操作。
定时器是软件开发中的一个重要组件。类似于一个"闹钟"。达到一个设定的时间之后,就执行某个指定好的代码。
定时器的使用场景:
-
定时提醒:日历应用程序或提醒应用程序可以使用定时器来触发提醒事件,例如在预定的时间点提醒用户参加会议或生日。
-
定时任务:定时关闭电视或空调等家用电器,以减少不必要的能源消耗。
-
游戏开发:在游戏开发中,定时器可以用于实现游戏中的动画效果、计时器功能或限时任务等。
二、Java标准库中的定时器
Java标准库中提供了一个 Timer 类,Timer 类的核心方法为 schedule。
schedule 方法包含两个参数。
- 第一个参数:指定即将要执行的任务代码,这个参数的类型 TimerTask 是一个抽象类,它实现了 Runnable 接口,就把它当作 Runnable 来使用即可。
- 第二个参数:指定多长时间之后执行任务(单位为毫秒)。
Timer 类内部包含一个线程,只有这个线程来执行所有的定时任务。这意味着如果有多个任务被安排在同一时间执行,它们将按顺序逐个执行,而不能并行执行。
import java.util.Timer;
import java.util.TimerTask;
public class Demo1 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Hello 3000");
}
}, 3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Hello 2000");
}
}, 2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Hello 1000");
}
}, 1000);
}
}
这段代码创建了一个 Timer
对象,并安排了三个定时任务,分别在延迟 3000 毫秒、2000 毫秒和 1000 毫秒后执行。
由于 Timer 内部包含了前台线程,因此进程没有结束。
三、自己实现定时器
需求:
- 能够延时执行任务/指定时间执行任务。
- 能够管理多个任务。
对于要延时执行的任务,要将其转换成绝对时间(当前的时间戳),这样方便判定后续任务是否要执行,因为如果还是记录延时,需要随着时间的推移不断更新delay,非常麻烦。
- 我们将任务及任务执行的绝对时间封装成一个类Task,更具体地表示这个任务。
- 用一个优先级队列保存这些任务,以任务执行的绝对时间靠前为优先级。(不使用 PriorityBlockingQueue,在这个实现中容易死锁)
- 定时器中需要有线程一直扫描队首元素,看队首是否需要执行。
详细过程见代码:
import java.util.PriorityQueue;
//用于描述一个任务的类
class MyTimerTask implements Comparable<MyTimerTask> {
//要执行的任务
private Runnable runnable;
//当前任务实际执行的时间戳
private long time;
public MyTimerTask(Runnable runnable, long delay) {
this.runnable = runnable;
//取当前时刻的时间戳+delay(延迟时间),作为当前任务实际执行的时间戳
this.time = System.currentTimeMillis() + delay;
}
public void run() {
this.runnable.run();
}
public long getTime() {
return this.time;
}
@Override
public int compareTo(MyTimerTask o) {
//试试哪个是升序就可以
return (int) (this.time - o.time);
//return (int) (o.time - this.time);
}
}
//定时器
class MyTimer {
//用优先级队列存放所有任务,以时间戳升序排序
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
//定时器中存在一个工作线程,不停扫描队首元素,看是否能执行这个任务
public MyTimer() {
Thread t = new Thread(() -> {
while (true) {
try {
synchronized (this) {
//任务队列为空,主动阻塞等待
if (queue.isEmpty()) {
this.wait();
}
//看队首元素是否到达要执行的时间
MyTimerTask task = queue.peek();
long curTime = System.currentTimeMillis();
//已经到达(当前时间的时间戳更大),任务执行并出队
if (curTime >= task.getTime()) {
task.run();
queue.poll();
} else {
//队首还没到达执行时间,则任务队列所有任务都还没到达执行时间
//避免重复循环判断,主动阻塞等待(等待的最长时间就是当前的时间间隔)
this.wait(task.getTime() - curTime);
}
}
} catch (InterruptedException e) {
throw new RuntimeException();
}
}
});
t.start();
}
//安排指定的任务,在指定的时间之后执行
public void schedule(Runnable runnable, long delay) {
synchronized (this) {
MyTimerTask task = new MyTimerTask(runnable, delay);
queue.offer(task); //加入到任务队列
//有新任务,就唤醒上次阻塞等待的任务
//当前新任务的实际执行时间可能更早,此时再判断队首任务是否执行,并更新wait的时间
this.notify();
}
}
}
//实现定时器
public class Demo2 {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 3000");
}
}, 3000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 2000");
}
}, 2000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 1000");
}
}, 1000);
}
}
执行结果于使用标准库的一致。这里不使用PriorityBlockingQueue的原因是:它只能处理队列为空时候的阻塞,而任务还都未到执行时间时的阻塞,就需要通过额外的锁对象和 wait 来实现。
此时代码就更复杂了,引入了两把锁(额外引入的锁对象和阻塞队列自带的锁),这就容易死锁了,而我们自己控制wait,只需要一把锁,更容易控制代码。
四、标准库中更推荐使用的定时器
由于 Timer
是单线程的,因此不推荐在任务中执行耗时操作或阻塞操作,因为这会影响到其他任务的执行。另外,如果在任务中抛出未捕获的异常,会导致该线程终止,从而影响到其他任务的执行。
Java标准库中还有一种主要的定时器实现,就是使用案例(3)中标准库的线程池 Executors 工厂类里的第四个工厂方法:Executors.newScheduledThreadPool(int corePoolSize
)。
这个方法返回ScheduledExecutorService,ScheduledExecutorService
是一种特殊类型的线程池,它具有定时执行任务的功能。它继承自 ExecutorService
接口,同时扩展了定时执行任务的能力。相比于 Timer
类,ScheduledExecutorService
支持更多的灵活性和功能,并且可以更好地处理多个并发任务。
ScheduledExecutorService
接口的核心方法也是 schedule,与 Timer 不同的是其参数。
- 参数
command
是一个Runnable
对象,表示要执行的任务; - 参数
delay
是延迟时间,以指定的时间单位(TimeUnit
)表示; - 参数
unit
则是时间单位,可以是纳秒、毫秒、秒等。
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Demo3 {
public static void main(String[] args) {
//使用 Executors.newScheduledThreadPool
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(1);
threadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 3000");
}
}, 3000, TimeUnit.MILLISECONDS);
threadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 2000");
}
}, 2000, TimeUnit.MILLISECONDS);
threadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 1000");
}
}, 1000, TimeUnit.MILLISECONDS);
threadPool.shutdown();
}
}