【JavaEE初阶】 定时器详解与实现

心已赠人 2024-04-17 10:31 142阅读 0赞

文章目录

  • ?定时器是什么
  • ?Java标准库中的定时器
  • ?模拟实现定时器
    • ?定时器的构成
    • ?第一步:MyStack类的建立
    • ?第二步:创建MyTimer类
    • ?第三步:解决相关问题
  • ?完整代码实现与测试
  • ⭕总结

?定时器是什么

在这里插入图片描述
定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码

定时器是一种实际开发中非常常用的组件.
比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.
比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除).
类似于这样的场景就需要用到定时器.

?Java标准库中的定时器

  • 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
  • schedule 包含两个参数.
  • 第一个参数指定即将要执行的任务代码,
  • 第二个参数指定多长时间之后执行 (单位为毫秒)

代码示例:

下面程序分别有一个定时器,设置了三个不同的时间

  1. import java.util.Timer;
  2. import java.util.TimerTask;
  3. public class TestDemo {
  4. public static void main(String[] args) {
  5. Timer timer = new Timer();
  6. System.out.println("程序启动!");
  7. timer.schedule(new TimerTask() {
  8. @Override
  9. public void run() {
  10. System.out.println("定时器3");
  11. }
  12. },3000);
  13. timer.schedule(new TimerTask() {
  14. @Override
  15. public void run() {
  16. System.out.println("定时器2");
  17. }
  18. },2000);
  19. timer.schedule(new TimerTask() {
  20. @Override
  21. public void run() {
  22. System.out.println("定时器1");
  23. }
  24. },1000);
  25. }
  26. }

运行结果如下:
在这里插入图片描述
结果如我们所示,按照时间顺序进行打印

?模拟实现定时器

首先我们先来看一下定时器的构成

?定时器的构成

  1. 一个带优先级的阻塞队列

    • 为啥要带优先级呢?
      因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的. 使用带优先级的队列就可以高效的把这个 delay 最小的任务找出来.
  2. 队列中的每个元素是一个 MyTask 对象.

  3. MyTask 中带有一个时间属性, 队首元素就是即将要执行的任务
  4. 同时有一个线程一直扫描队首元素, 看队首元素是否需要执行

?第一步:MyStack类的建立

包含两个属性

  • 包含一个 Runnable 对象
  • 一个 time(毫秒时间戳)

由于我们的MyTask类需要放入一个带优先级的阻塞队列中,所以我们需要MyTack可以比较,这里博主选择重写 Comparable 接口里的compareTo方法

代码实现如下:

  1. public class MyTask implements Comparable<MyTask> {
  2. private Runnable runnable;
  3. private long time;
  4. public MyTask() {
  5. System.out.println(1);
  6. }
  7. public void tad() {
  8. System.out.println(2);
  9. }
  10. public MyTask(Runnable runnable, long time) {
  11. this.runnable = runnable;
  12. this.time = time;
  13. }
  14. public long gettime(MyTask) {
  15. return this.time;
  16. }
  17. //执行任务
  18. public void run() {
  19. runnable.run();
  20. }
  21. @Override
  22. public int compareTo(MyTask o) {
  23. return (int)(this.time - o.time);
  24. }
  25. }

?第二步:创建MyTimer类

该类需要有一个带有优先级的阻塞队列

还需要有一个可schedule 方法用于我们来插入我们我们需要执行的任务

  1. public class MyTimer {
  2. // 有一个阻塞优先级队列, 来保存任务.
  3. private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
  4. // 指定两个参数
  5. // 第一个参数是 任务 内容
  6. // 第二个参数是 任务 在多少毫秒之后执行. 形如 1000
  7. public void schedule(Runnable runnable, long after) {
  8. // 注意这里的时间上的换算
  9. MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);
  10. queue.put(task);
  11. }
  12. }
  • 由于我们输入的都是一个时间大小,所以我们需要进行处理一下,
  • 这里的System.currentTimeMillis()是获取当时的时间戳
  • 再加上所需要的时间大小即达到我们效果

其次还需要一个线程循环扫描

  • 该扫描我们需要做的是
  • 取出队首元素, 检查看看队首元素任务是否到时间了.
  • 如果时间没到, 就把任务塞回队列里去.
  • 如果时间到了, 就把任务进行执行.

    private Thread t = null;

    1. public MyTimer() {
    2. t = new Thread() {
    3. @Override
    4. public void run() {
    5. while (true) {
    6. try {
    7. // 取出队首元素, 检查看看队首元素任务是否到时间了.
    8. // 如果时间没到, 就把任务塞回队列里去.
    9. // 如果时间到了, 就把任务进行执行.
    10. MyTask myTask = queue.take();
    11. long curTime = System.currentTimeMillis();
    12. if (curTime < myTask.getTime()) {
    13. // 还没到点, 先不必执行
    14. // 现在是 13:00, 取出来的任务是 14:00 执行
    15. //塞回去
    16. queue.put(myTask);
    17. } else {
    18. // 时间到了!! 执行任务!!
    19. myTask.run();
    20. }
    21. } catch (InterruptedException e) {
    22. e.printStackTrace();
    23. }
    24. }
    25. }
    26. };
    27. //启动线程
    28. t.start();
    29. }

?第三步:解决相关问题

  • 问题一:while (true) 转的太快了, 造成了无意义的 CPU 浪费.

比如第一个任务设定的是 1 min 之后执行某个逻辑. 但是这里的 while (true) 会导致每秒钟访问队首元素几万次.

解决办法:引入一个locker对象, 借助该对象的 wait / notify 来解决 while (true) 的忙等问题.

我们在循环扫描里:引入 wait, 等待一定的时间.并修改 MyTimer 的 schedule 方法, 每次有新任务到来的时候唤醒一下循环扫描线程. (因为新插入的任务可能是需要马上执行的)

  • 问题二:原子性问题

由于我们的出队列操作和判断语句不具有原子性

问题情况如下:

出队列操作拿到任务后,还没有进行判断

然后这时候有一个来了一个新任务
在这里插入图片描述
但是此时我们该任务还没有wait()操作,而且我们由于添加新元素,notify()操作已执行,这就导致后面的wait操作不会被唤醒,那么新来的任务就在相应时间来没有被执行

解决方法:将出队列操作与判断操作都加上锁

代码实现如下:

  1. import java.util.concurrent.PriorityBlockingQueue;
  2. public class MyTimer {
  3. // 有一个阻塞优先级队列, 来保存任务.
  4. private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
  5. // 扫描线程
  6. private Thread t = null;
  7. private Object locker = new Object();
  8. public MyTimer() {
  9. t = new Thread() {
  10. @Override
  11. public void run() {
  12. while (true) {
  13. try {
  14. // 取出队首元素, 检查看看队首元素任务是否到时间了.
  15. // 如果时间没到, 就把任务塞回队列里去.
  16. // 如果时间到了, 就把任务进行执行.
  17. synchronized (locker) {
  18. MyTask myTask = queue.take();
  19. long curTime = System.currentTimeMillis();
  20. if (curTime < myTask.getTime()) {
  21. // 还没到点, 先不必执行
  22. // 现在是 13:00, 取出来的任务是 14:00 执行
  23. queue.put(myTask);
  24. // 在 put 之后, 进行一个 wait
  25. locker.wait(myTask.getTime() - curTime);
  26. } else {
  27. // 时间到了!! 执行任务!!
  28. myTask.run();
  29. }
  30. }
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. }
  36. };
  37. t.start();
  38. }
  39. // 指定两个参数
  40. // 第一个参数是 任务 内容
  41. // 第二个参数是 任务 在多少毫秒之后执行. 形如 1000
  42. public void schedule(Runnable runnable, long after) {
  43. // 注意这里的时间上的换算
  44. MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);
  45. queue.put(task);
  46. synchronized (locker) {
  47. locker.notify();
  48. }
  49. }
  50. }

?完整代码实现与测试

计时器完整代码:

  1. import java.util.concurrent.PriorityBlockingQueue;
  2. class MyTask implements Comparable<MyTask> {
  3. private Runnable runnable;
  4. private long time;
  5. public MyTask() {
  6. System.out.println(1);
  7. }
  8. public void tad() {
  9. System.out.println(2);
  10. }
  11. public MyTask(Runnable runnable, long time) {
  12. this.runnable = runnable;
  13. this.time = time;
  14. }
  15. public long getTime() {
  16. return this.time;
  17. }
  18. //执行任务
  19. public void run() {
  20. runnable.run();
  21. }
  22. @Override
  23. public int compareTo(MyTask o) {
  24. return (int)(this.time - o.time);
  25. }
  26. }
  27. public class MyTimer {
  28. // 有一个阻塞优先级队列, 来保存任务.
  29. private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
  30. // 扫描线程
  31. private Thread t = null;
  32. private Object locker = new Object();
  33. public MyTimer() {
  34. t = new Thread() {
  35. @Override
  36. public void run() {
  37. while (true) {
  38. try {
  39. // 取出队首元素, 检查看看队首元素任务是否到时间了.
  40. // 如果时间没到, 就把任务塞回队列里去.
  41. // 如果时间到了, 就把任务进行执行.
  42. synchronized (locker) {
  43. MyTask myTask = queue.take();
  44. long curTime = System.currentTimeMillis();
  45. if (curTime < myTask.getTime()) {
  46. // 还没到点, 先不必执行
  47. // 现在是 13:00, 取出来的任务是 14:00 执行
  48. queue.put(myTask);
  49. // 在 put 之后, 进行一个 wait
  50. locker.wait(myTask.getTime() - curTime);
  51. } else {
  52. // 时间到了!! 执行任务!!
  53. myTask.run();
  54. }
  55. }
  56. } catch (InterruptedException e) {
  57. e.printStackTrace();
  58. }
  59. }
  60. }
  61. };
  62. t.start();
  63. }
  64. // 指定两个参数
  65. // 第一个参数是 任务 内容
  66. // 第二个参数是 任务 在多少毫秒之后执行. 形如 1000
  67. public void schedule(Runnable runnable, long after) {
  68. // 注意这里的时间上的换算
  69. MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);
  70. queue.put(task);
  71. synchronized (locker) {
  72. locker.notify();
  73. }
  74. }
  75. }

测试代码如下

  1. public class TestDemo2 {
  2. public static void main(String[] args) {
  3. MyTimer myTimer = new MyTimer();
  4. System.out.println("程序启动");
  5. myTimer.schedule(new Runnable() {
  6. @Override
  7. public void run() {
  8. System.out.println("计时器3");
  9. }
  10. },3000);
  11. myTimer.schedule(new Runnable() {
  12. @Override
  13. public void run() {
  14. System.out.println("计时器2");
  15. }
  16. },2000);
  17. myTimer.schedule(new Runnable() {
  18. @Override
  19. public void run() {
  20. System.out.println("计时器1");
  21. }
  22. },1000);
  23. }
  24. }

测试结果如下:
在这里插入图片描述

⭕总结

关于《【JavaEE初阶】 定时器详解与实现》就讲解到这儿,感谢大家的支持,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下!

发表评论

表情:
评论列表 (有 0 条评论,142人围观)

还没有评论,来说两句吧...

相关阅读