12. 多线程(9) --- 案例:定时器
文章目录
- 前言
- 1. 什么是定时器
- 2. 标准库中的定时器
- 3. 自己实现定时器
前言
我们在上个博客中学习到了 阻塞队列的使用和模拟实现,这次我们学习 定时器。
1. 什么是定时器
定时器是软件开发中的一个重要组件。类似于一个 “闹钟”。达到一个设定的时间之后,就执行某个指定好的代码。
定时器是一种实际开发中非常常用的组件。
比如在网络通信中,如果对方 500ms 内没有返回数据,则断开连接尝试重连
比如一个 Map,希望里面的某个 key 在 3s 之后过期 (自动删除)
类似于这样的场景就需要用到定时器
2. 标准库中的定时器
- 标准库中提供了一个Timer类,Timer类的核心方法为 schedule.
- schedule 包含两个参数,第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒)。
有两种实现方法:
1.
public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Hello world");}},300);}
public static void main(String[] args) {TimerTask timerTask = new TimerTask() {@Overridepublic void run() {System.out.println("Hello World");}};Timer timer = new Timer();timer.schedule(timerTask,3000);}
这两种方法都行。
一般来说,是对 Runnable 进行重写,在定时器这 TimeTask 用 Runnable 封账了一下。
我们写一下下面的代码,观察效果。
public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Hello 3000");}},3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Hello 2000");}},2000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Hello 1000");}},1000);}
我们发现这个代码和上面的代码在运行完之后,程序是不会停下来的,这是因为:
当 打印完 “Hello 3000” 在 3000ms 打印完之后,并不意味着 Timer 结束.
实际上是,Timer 线程可能还在等待其他任务 (如果有的话) 或者处于空闲状态等待新的任务。由于并没有什么其他任务提交给 Timer,所以当Timer中所有的任务执行完毕之后,就会等待,也可以显式调用 timer.cancel() 提前结束。
可以使用下面的代码,来观察效果。
public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Hello 3000");timer.cancel();}},3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Hello 2000");}},2000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Hello 1000");}},1000);}
3. 自己实现定时器
根据上面的解释,其实可以发现构建定时器是要通过 阻塞队列,直观点就是 带有 优先级 的 阻塞队列。
以下思考点:
- 一个任务一个类,这样来表示吗?
这个就是返回到最早学习Java语法,1,2,3,4, 这么多的数,需要用a,b,c不同的变量来保存吗,可以直接使用数组来存储。因此,我们可以使用集合类把每个任务存储在一块。
- 用什么集合类,使用 ArrayList 可以吗?
任务是有执行时机的,我们把任务存储在ArrayList中,那就需要遍历每个任务的时机,任务早的先执行。
- 那么如何可以通过在创建集合的时候,就可以按照执行的先后顺序,把任务存储到集合中呢?
我们可以使用 堆 来执行。
我们首先创建一个类,既包含任务本身,又包含时间。
class MyTimerTask implements Comparable<MyTimerTask>{private long time;private Runnable task;public MyTimerTask(long time, Runnable task) {this.time = time;this.task = task;}@Overridepublic int compareTo(MyTimerTask o) {return (int)(this.getTime()-o.getTime());}public void run(){task.run();}public long getTime(){return this.time;}
}
在 MyTimer 中,我们在初始化过程中,使用一个线程负责执行队列中的任务,并且写 schedule方法来向优先级队列添加任务。
class MyTimer{// 优先级队列private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();private Object locker = new Object();public MyTimer(){Thread t = new Thread(()->{while (true){if (queue.isEmpty()){continue;}MyTimerTask task = queue.peek();if (System.currentTimeMillis() < task.getTime()){// 如果当前时间比 任务的时间少,那么说明还没有到达指定的时间continue;}else {task.run();queue.poll();}}});t.start();}public void schedule(Runnable task,long delay){MyTimerTask myTimerTask = new MyTimerTask(task,delay+System.currentTimeMillis());queue.offer(myTimerTask);}
}
这样子写是一定不行的,我们对上面的代码需要进行修改。
- 在添加任务的时候,需要做到有序,需要上锁来处理。
public void schedule(Runnable task,long delay){synchronized (locker){MyTimerTask myTimerTask = new MyTimerTask(task, delay + System.currentTimeMillis());queue.offer(myTimerTask);locker.notify();}}
- 上面有两处 continue ,这个方法会死等,一直消耗 cpu 的资源,我们需要根据运行时间来进行分配。
Thread t = new Thread(()->{try {while (true){if (queue.isEmpty()){locker.wait();}MyTimerTask task = queue.peek();if (System.currentTimeMillis() < task.getTime()){// 如果当前时间比 任务的时间少,那么说明还没有到达指定的时间locker.wait(task.getTime()-System.currentTimeMillis());continue;}else {task.run();queue.poll();}}}catch (InterruptedException e) {throw new RuntimeException(e);}});
总的代码:
import java.nio.FloatBuffer;
import java.util.PriorityQueue;/*** @Author: XXHH* @CreateTime: 2025-05-02*/
class MyTimerTask implements Comparable<MyTimerTask>{private long time;private Runnable task;public MyTimerTask(Runnable task,long time) {this.time = time;this.task = task;}@Overridepublic int compareTo(MyTimerTask o) {return (int)(this.getTime()-o.getTime());}public void run(){task.run();}public long getTime(){return this.time;}
}
class MyTimer{// 优先级队列private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();private Object locker = new Object();public MyTimer(){Thread t = new Thread(()->{try {while (true){if (queue.isEmpty()){locker.wait();}MyTimerTask task = queue.peek();if (System.currentTimeMillis() < task.getTime()){// 如果当前时间比 任务的时间少,那么说明还没有到达指定的时间locker.wait(task.getTime()-System.currentTimeMillis());continue;}else {task.run();queue.poll();}}}catch (InterruptedException e) {throw new RuntimeException(e);}});t.start();}public void schedule(Runnable task,long delay){synchronized (locker){MyTimerTask myTimerTask = new MyTimerTask(task, delay + System.currentTimeMillis());queue.offer(myTimerTask);locker.notify();}}
}
public class Demo41 {public static void main(String[] args) {MyTimer timer = new MyTimer();timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello 3000");}}, 3000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello 2000");}}, 2000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello 1000");}}, 1000);}
}
下一个我们讲解 线程池,我们不见不散!