多线程——定时器
目录
1.Java标准库中的定时器
1.1 简单认识定时器
1.2 核心方法源码解读
2.模拟实现定时器
2.1 实现
2.2 测试
3.小结
上期学习到线程池,本期再来学习多线程的另一个应用,也就是定时器。
1.Java标准库中的定时器
1.1 简单认识定时器
早八人,早八魂,相信大家早上为了赶个早八都会定不少闹钟⏰,以此来叫醒自己。在Java中,有时候我们需要某些代码可以在指定时间之后再执行,就需要用到定时器。
定时器是一种重要的组件,标准库中提供了 Timer 类,也就是定时器,Timer 类的核心方法是 schedule 。
schedule 方法有两个参数。第一个参数是要执行的逻辑代码任务,并在里面重写 run() 方法。第二个参数是希望指定多长时间之后这个代码再执行,单位是毫秒(ms)。在调用 schedule 方法时,要传入 new TimerTask ,相当于是定时器的任务,随着调用 schedule 方法开始而创建。
这里举一个简单的代码例子:
public class Test {public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("3秒后执行");}} ,3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("2秒后执行");}} ,2000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("1秒后执行");}} ,1000);System.out.println("main执行");}
}
代码解读:这里调用三次 schedule 方法,根据以往的学习,这里没有线程等待,代码应该按顺序依次打印。但是当允许代码时,并非如此:
从结果可以看出,先打印“main执行”,然后再依次按时间打印。
这就是定时器,有的时候我们会希望一些代码在规定的时间后再执行,就可以使用到定时器。
1.2 核心方法源码解读
从上面的代码中可以看出,并不是说哪个任务先有,就会先执行哪个任务,而是根据时间来定的,即时间先到的任务会先执行。我们第一时间可能会先想到阻塞队列(BlockingQueue),因为在这里好像发生了一定的等待,在前期我们又刚好接触到阻塞队列,但是这里的实现方式并不是阻塞队列,阻塞队列会造成死锁。所以在这里,用到的其实是优先级队列(PriorityQueue),根据时间戳和当前代码的执行时间做差得到的时间优先级来进行排列。时间戳简单理解就是当前此刻的时间。在 Timer 类源码中,就有对这个队列的实现。在后面实现简单的定时器时,就需要用到优先级队列。
(关于优先级队列的内容,可以转移至 数据结构——优先级队列 学习)
这里只展示核心方法的调用,想要深入理解 Timer 类,还是需要看 Timer 类的源码。
2.模拟实现定时器
2.1 实现
想要实现一个定时器,需要理解四个步骤:
- 创建一个类,表示要执行的任务
- 需要执行的任务需要根据时间先后来管理,试用优先级队列
- 实现核心方法schedule
- 额外创建一个线程,负责执行队列中的任务
相关的注释都在代码中写出。
任务类:
/*** 自定义定时任务类 - 封装要执行的任务和执行时间* 实现 Comparable 接口用于在优先级队列中排序*/
public class MyTimerTask implements Comparable<MyTimerTask> {// 要执行的任务 - 使用 Runnable 接口提供任务执行逻辑private Runnable task;// 任务的执行时间 - 使用绝对时间戳// 表示任务应该在什么时间点执行private long time;// 构造函数 - 创建定时任务对象public MyTimerTask(Runnable task, long time) {this.task = task;this.time = time;}// 获取任务的执行时间public long getTime() {return this.time;}// 执行任务 - 这个方法通常由定时器的工作线程调用public void run() {this.task.run();}// 比较方法 - 实现 Comparable 接口用于任务排序// 按照任务的执行时间进行升序排序(时间越早优先级越高)// 负数 - 当前任务执行时间早于参数任务// 零 - 两个任务执行时间相同// 正数 - 当前任务执行时间晚于参数任务@Overridepublic int compareTo(MyTimerTask o) {// 注意:直接相减可能在时间差很大时溢出// 更安全的实现方式:Long.compare(this.time, o.time)return (int) (this.time - o.time);}
}
线程类:
/*** 自定义定时器类 - 基于优先级队列实现的简单实现* 支持延迟执行任务,按照执行时间顺序调度*/
public class MyTimer {//优先级队列存放任务private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>() ;//锁,会涉及等待private Object locker = new Object();//schedule方法(第一个参数表示要执行的代码逻辑,第二个参数表示要执行的时间public void schedule(Runnable task, long delay) {synchronized (locker) {//希望多久执行,当前时间戳+延迟时间MyTimerTask myTimerTask = new MyTimerTask(task, System.currentTimeMillis() + delay);queue.offer(myTimerTask);//任务加入队列locker.notify();//唤醒等待}}//构造方法 --public MyTimer(){//线程创建Thread t = new Thread(() -> {try {while (true) {synchronized (locker) {//需要注意这里用while,因为在多线程环境下,可能会造成虚假唤醒,while相当于二次判断while (queue.isEmpty()) {locker.wait();//如果是空队列,就等待}MyTimerTask task = queue.peek();//查看队首任务if (task.getTime() > System.currentTimeMillis()) {//如果要执行的时间大于当前时间戳,继续等待//这里不能再用while,因为可能有新任务加入locker.wait(task.getTime() - System.currentTimeMillis());//为了避免虚假唤醒,还可以再判断一次if (task.getTime() > System.currentTimeMillis()) {continue;//如果真是虚假唤醒,就退出这一次循环,重新开始等待}} else {//任务执行task.run();//当前任务执行后出队queue.poll();}}}} catch (InterruptedException e) {e.printStackTrace();}});//线程启动,这里尤其不能忘记t.start();;}
}
以上是一个简单的计时器,下面来进行测试。
2.2 测试
测试代码:
public class Test {public static void main(String[] args) {MyTimer myTimer = new MyTimer();myTimer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("3秒后执行");}} ,3000);myTimer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("2秒后执行");}} ,2000);myTimer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("1秒后执行");}} ,1000);System.out.println("main执行");}
}
运行结果:
从结果可以发现,我们实现的这个简单定时器,和标准库中的 Timer 类的结果是一样的。
3.小结
在Java多线程编程中,定时器是一个重要的组件,允许我们的逻辑代码在指定时间之后再执行。Java标准库中提供的 Timer 类核心方法是 schedule ,这个方法能够安排任务在指定延迟后再执行。定时器并不是按照任务的添加顺序来执行,而是根据任务的执行时间优先级来调度,这种调度机制依赖于优先级队列。
我们通过实现一个简单的定时器,掌握了定时器的核心设计思路:
- 创建一个类,表示要执行的任务
- 需要执行的任务需要根据时间先后来管理,试用优先级队列
- 实现核心方法schedule
- 额外创建一个线程,负责执行队列中的任务
在实现过程中,尤其要注意多线程环境下的线程安全问题,因此使用了同步锁和 wait/notify 机制来协调任务的添加和执行,同时更要注意使用 while循环来避免虚假唤醒问题。