目录
🍃1.定时器是一种实际开发中非常常用的组件 .🍃2.比如网络通信中 , 如果对方 500ms 内没有返回数据 , 则断开连接尝试重连 .🍃3.比如一个 Map, 希望里面的某个 key 在 3s 之后过期 ( 自动删除 ).🍃4.类似于这样的场景就需要用到定时器 .
- public class TestDemo1 {
- public static void main(String[] args) throws InterruptedException {
- // java.util 里的一个组件
- Timer timer = new Timer();
- // schedule : 安排一个任务
- // 该任务不是立刻执行,而是延迟多少时间后再执行!!
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- System.out.println("这是一个要执行的任务!");
- }
- },3000);
- // 定时器 和 sleep 不相同
- while(true) {
- System.out.println("main");
- Thread.sleep(1000);
- }
- }
- }
🍁【注意事项】
🍃1.定时器的核心方法是 schedule, schedule 包含两个参数,第一个参数指定即将要执行的任务,第二个参数指定多长时间后执行(单位:毫秒)。
🍃2.将定时器和 sleep 做区分,sleep 是使当前线程处于阻塞状态,而 定时器 只是记录了多长时间后该执行的任务,中间的这些时间,当前线程该干嘛就干嘛。
🍃3.schedule 里面的 TimerTask 其实就相当于 Runnable,只不过是 TimerTask 实现了 Runnable 接口,在这里我们直接把它当成 Runnable 就好了。
通过观察标准库中的定时器,我们大概知道要怎么做了!!
🍃1. Timer 内部要组织很多的任务;
🍃2. Timer 里的每个任务都要通过一定的方式来描述出来;(自己定义一个 TimerTask)
🍃3. 还需要有一个线程,通过这个线程来扫描定时器内部的任务,执行其中时间到了的任务。(Timer 内部的线程)
🍁【代码实现】
- class MyTask implements Comparable
{ - // 任务
- private Runnable command;
- // 任务开始执行的时间(相对时间)
- private long time;
-
- public MyTask(Runnable command, long after) {
- this.command = command;
- // 绝对时间戳
- this.time = System.currentTimeMillis() + after;
- }
- // 执行任务的方法,直接在内部调用 Runnable 的 run 方法即可
- public void run() {
- command.run();
- }
-
- public long getTime() {
- return time;
- }
-
- @Override
- public int compareTo(MyTask o) {
- return (int) (this.time - o.time);
- }
- }
- //自己创建的定时器类
- class MyTimer {
- // 用来阻塞等待的锁对象
- private Object locker = new Object();
-
- // 使用优先级阻塞队列来保存若干个任务
- private PriorityBlockingQueue
queue = new PriorityBlockingQueue<>(); -
- // command : 要执行的任务是啥
- // after : 任务啥时候执行
- public void schedule(Runnable command, long after) {
- MyTask myTask = new MyTask(command, after);
- synchronized (locker) {
- queue.put(myTask);
- locker.notify();
- }
- }
- public MyTimer() {
- //在这里启动一个线程
- Thread t = new Thread(() -> {
- while(true) {
- //循环过程中,就不断的尝试从队列中获取到队首元素
- //判断队首元素当前的时间是否就绪,如果就绪了就执行,不就绪,就不执行
- try {
- synchronized (locker) {
- //队列为空,也要等待
- if(queue.isEmpty()) {
- locker.wait();
- }
- //取出队首任务
- MyTask myTask = queue.take();
- long curTime = System.currentTimeMillis();
- if(myTask.getTime() > curTime) {
- //时间还没到,就放回去
- queue.put(myTask);
- //等待最早的任务开始的时间和当前时间的差值
- locker.wait(myTask.getTime() - curTime);
- } else {
- //时间到了,就执行任务
- myTask.run();
- }
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- });
- t.start();
- }
- }
【分析实现过程中的重点步骤】
🍁重点 1:使用优先级阻塞队列将我们的任务组织起来!!
🍃1.虽然任务可能有很多,但是它们的执行顺序是一定的,且按照时间顺序先后来执行的,所以使用优先级队列。
🍃2.在多线程环境下,这个队列会被多个线程访问,第一,schedule 可能是在多线程中被调用,每次调用都要往队里添加元素;第二,定时器内部还需要有专门的线程来执行队列里的任务。这些操作在多线程里都是存在线程安全问题的,所以需要使用到优先级阻塞队列!!
🍁重点2:优先级阻塞队列里的元素是一个引用类型(MyTask),所以我们需要指定比较规则,既可以让 MyTask 实现 Comparable 接口,也可以在优先级阻塞队列的构造方法中传参,传一个比较器 Comparator !!
🍁重点3:执行任务的时候:队列不为空时,我们先将元素取出来,判断是否到了执行时间,没到时间就放回去,放回去之后要加上 wait() 。那么加入新任务的时候也就相应的要 notify() ,避免让 CPU 出现"空转"的现象!!
🍁重点4:我们的加锁不能只包裹 wait() ,notify(),否则会出现非原子性操作,从而导致线程安全问题!!
经过上述分析,我们发现多线程的代码真的是防不胜防,稍微一点不注意,都可能引起线程安全问题!!