目录
1. 定时器
2. 标准库中的定时器
3. 实现定时器
3.1 创建带优先级的阻塞队列
3.2 创建MyTask类
3.3 构建schedule方法
3.4 构建timer类中的线程
3.5 思考
1. 定时器
定时器也是软件开发中的一个重要组件. 类似于一个 "闹钟". 达到一个设定的时间之后, 就执行某个指定好的代码.
2. 标准库中的定时器
- 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
- schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).
使用步骤:
1. 实例化Timer对象
2.调用timer.schedule("任务",执行时间)
public class timerTest {public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello4");}},4000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello3");}},3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello2");}},2000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello1");}},1000);System.out.println("hello");}
}
运行结果:先打印主进程的hello,后面陆续按照指定的时间进行打印每个线程的内容
3. 实现定时器
定时器的构成
- 一个带优先级的阻塞队列
- 队列中的每个元素都是一个Task对象
- Task 中带有一个时间属性, 队首元素就是即将执行的任务
- 同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行
为什么使用带有优先级的堵塞队列?
答案:因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的. 使用带优先级的队列就可以高效的把这个 delay 最小的任务找出来.
3.1 创建带优先级的阻塞队列
3.2 创建MyTask类
队列中的每个元素都是一个Task对象,创建Mytask类,用来描述要执行的任务,以及执行的时间.(用于传入堵塞队列)
此处需要注意:
我们需要将Mytask类实现Comparable接口,根据执行的时间进行比较. 这样才能传入带有优先级的堵塞队列.
最终MyTask类代码为:
class MyTask implements Comparable<MyTask>{public Runnable runnable;public long time;public MyTask(Runnable runnable, long delay){// 取当前时刻的时间戳 + delay = 当前该任务实际执行的时间戳this.time = System.currentTimeMillis() + delay;this.runnable = runnable;}@Overridepublic int compareTo(MyTask o) {//每次取出的是时间最小的元素return (int)(this.time -o.time);}
}
3.3 构建schedule方法
通过schedule方法往队列中插入Task对象
3.4 构建timer类中的线程
Timer 类中存在一个 worke 线程, 一直不停的扫描队首元素, 看看是否能执行这个任务.
此线程的实现思路
- 1. 线程要执行在队列中不断地取出任务 queue.take();
- 2.取出任务要进行比较当前系统时间与任务执行时间
- 3.如果任务执行时间小于当前系统时间,就说明要执行任务了.调用 myTask.runnable.run();
- 4.如果当前取出的任务执行时间大于当前系统时间,就说明任务还没有到执行时间,将任务推送到队列中.同时进入堵塞等待
- 5. 在schedule方法中,往优先级队列推送任务之后,同时加一个notify方法,用来唤醒此时正在堵塞的线程,使得堵塞等待解除,重新取队首任务进行比较时间.
加wait notify的好处,就是work线程不需要一直进行取队首元素,这样会消耗系统资源,造成没必要的浪费,只需要等待堵塞当前距离执行任务的时间差就可以,当有新的任务添加进来的时候接触堵塞,重新进行计算时间差,再决定是否进行执行任务,还是进入堵塞状态.
public class MyTimer {private final PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();// 创建一个锁对象private final Object locker = new Object();public MyTimer(){//1.创建一个线程Thread work = new Thread(()->{while (true){try {synchronized (locker) {//2.队列中取出一个任务MyTask myTask = queue.take();//3 获取当前时间long curTime = System.currentTimeMillis();//4. 任务执行时间与当前时间进行对比if (myTask.time <= curTime){//4.1 任务执行时间小于等与当前时间,说明应该要执行任务了myTask.runnable.run();}else {//4.1 任务执行时间大于等与当前时间,说明该任务还没有到执行的时间,再将刚才取出的任务放回原来的队列queue.put(myTask);locker.wait(myTask.time-curTime);//针对这个wait()://1.方便随时唤醒,比如当前时刻是14:00,约定14:30要执行上课任务,//此时取出队首元素,发现时间没有到,就wait(任务执行时间-当前时间)//2.当新的任务来了,需要比之前的队伍提前执行,那么就需要进行唤醒之前的wait(),//重新取队首元素,进行比较时间,确定wait()的时间.}}} catch (InterruptedException e) {throw new RuntimeException(e);}}});work.start();}public void schedule(Runnable runnable, long delay){// 根据参数,构造MyTask,插入队列MyTask myTask = new MyTask(runnable,delay);queue.put(myTask);synchronized (locker){locker.notify();}}
}
3.5 思考
我们将锁加在了整个执行任务.此时我们如果只针对wait进行加锁?这样线程安全吗?不安全的话给出理由.
答案:会出现线程不安全的情况
比如下图解释:
我们此时有两个线程,T1线程此时取出一个任务(执行时间为14:30),比较当前时间(14:00),还没有到执行时间,此时将任务推送给队列,但是在推送之前,此时有一个T2线程,正在插入一个新的任务(14:10),同时执行了notify操作,但是此时T1线程并没有wait,此时就空打一炮,此时T1线程开始拿到锁,进行堵塞等待,但是此时等待的时间为(14:30 - 14:00),T2线程插入的新任务还有10分钟需要执行,但是因为之前已经notify一次,此时堵塞的时间无法进行唤醒操作,所以T2线程插入的这个任务要等到14:30才能执行,这就引起线程的不安全.
当我们加锁在整个(取出任务和推送任务),T1线程一定在wait之前不会使得T2线程执行notify操作,因为T1线程在加锁中,等待wait之后才会解除锁,等待T1的锁解除,T2才会执行notify操作. 进而去唤醒T1线程中的wait.