• 【JavaEE初阶】多线程 _ 基础篇 _ 定时器(案例三)


    ☕导航小助手☕

          🍚写在前面

                🥡一、定时器概述

                🍜二、定时器的实现

                     🍔🍔2.1 Java标准库 定时器的使用

                     🧇🧇2.2 自己模拟实现一个定时器

                     🦪🦪2.3 对自己实现的定时器的进一步优化

                                🍣🍣🍣2.3.1 为何需要再进行优化

                                🍤🍤🍤2.3.2 如何进行进一步优化(一)

                                🥩🥩🥩2.3.3 如何进行进一步优化(二)

                     🍱🍱2.4 附上自己模拟实现定时器的代码


    写在前面

    继多线程的阻塞队列之后,我们来学习一下 第三个经典的案例 —— 定时器~

    定时器,也是开发中的一个比较常用的基础组件~

    定时器,就像闹钟一样,闹钟在达到一个设定的时间以后,就会发出响声来提醒;而定时器到了设定好的时间以后,就会执行某个指定好的代码~

    下面,让我们一起来看看吧~

    一、定时器概述

    定时器,又称 任务定时执行器~

    即:到了一定的时间,就会去执行指定的任务~

    在 Java标准库中,提供了一个带有定时器功能的 Timer类,Timer类 的核心方法是 schedule方法~

    schedule方法 包含了两个参数,第一个参数为 即将要执行的任务代码,第二个参数为 指定多长时间后执行地一个参数中的代码(单位是 毫秒)~

    如:

    1. Timer timer = new Timer();
    2. timer.schedule(new TimerTask() {
    3.    @Override
    4.    public void run() {
    5.        System.out.println("hello");
    6.   }
    7. }, 3000);

    二、定时器的实现

    2.1 Java标准库 定时器的使用

    1. package thread;
    2. import java.util.Timer;
    3. import java.util.TimerTask;
    4. public class Demo23 {
    5. public static void main(String[] args) {
    6. //Timer 是 java.util 里的一个组件
    7. Timer timer = new Timer();
    8. //schedule 这个方法的效果是 "安排一个任务",不是立即执行,而是等到设定好的时间之后再执行
    9. timer.schedule(new TimerTask() {
    10. @Override
    11. public void run() {
    12. System.out.println("这是一个要执行的任务");
    13. }
    14. },4000);
    15. }
    16. }

    运行结果:


    在观察运行结果之后,就会提出两个小问题:

    问题一:程序执行完之后,为啥没有结束呢?

    实现定时器,背后涉及到了多线程,Timer 里面有线程,这个线程的运行阻止了 进程的退出~


    问题二:那为什么不直接使用 sleep 呢,还专门搞了一个定时器?

    使用 sleep 是把当前线程给阻塞了~

    换句话说,sleep 的时间里,啥也干不了,只能干等着;但是使用定时器,之前的线程该干啥干啥~

    我们可以来证实一下,加一些代码并且观察运行结果:

    1. package thread;
    2. import java.util.Timer;
    3. import java.util.TimerTask;
    4. public class Demo23 {
    5. public static void main(String[] args) throws InterruptedException {
    6. //Timer 是 java.util 里的一个组件
    7. Timer timer = new Timer();
    8. //schedule 这个方法的效果是 "安排一个任务",不是立即执行,而是等到设定好的时间之后再执行
    9. timer.schedule(new TimerTask() {
    10. @Override
    11. public void run() {
    12. System.out.println("这是一个要执行的任务");
    13. }
    14. },5000);
    15. while (true) {
    16. System.out.println("main");
    17. Thread.sleep(1000);
    18. }
    19. }
    20. }

     运行结果:

    我们从中可以看出,在 定时器定时的那段时间里,线程是在继续干活的,仍然在打印出 main~

    如果使用 sleep,那么这段时间就只能干等了~

    所以,平常使用的还是定时器较多~

    2.2 自己模拟实现一个定时器

    在模拟实现定时器之前,我们应该要思考清楚,一个定时器里面应该要有什么~

    首先,我们要清楚,一个 Timer 内部其实是可以加入很多很多任务的~

    其次,Timer 里的每一个任务都要通过一定的方式描述出来(描述任务的过程 其实就相当于自己定义 TimerTask)~

    虽然当前的任务有很多,但是它们的执行顺序是一定的:按照时间顺序先后来执行~

    此处 可以使用 优先级队列来执行,使用优先级队列可以非常高效的 找到当前时间最小的任务(首先要执行的任务)~

    最后,还需要有一个线程,通过这个线程来扫描定时器 内部的任务,并执行其中时间到了的任务~


    1. package thread;
    2. import java.util.PriorityQueue;
    3. import java.util.concurrent.PriorityBlockingQueue;
    4. //通过这个类来描述一个任务
    5. class MyTask implements Comparable<MyTask> {
    6. //获取具体任务
    7. private Runnable command;
    8. //执行任务时的时间戳
    9. private long time;
    10. //构造方法
    11. public MyTask (Runnable command,long after) {
    12. this.command = command;
    13. //我们希望知道完整的时间戳(几点几分几秒之后执行),所以用 当前系统时间戳+多长时间后执行 来表示
    14. //此处记录的是绝对时间戳,不是 "多长时间之后执行"
    15. this.time = System.currentTimeMillis() + after;
    16. }
    17. //执行任务的方法,直接在内部调用 Runnable 的 run 即可
    18. public void run() {
    19. command.run();
    20. }
    21. //获取执行时间
    22. public long getTime() {
    23. return time;
    24. }
    25. @Override
    26. public int compareTo(MyTask o) {
    27. //希望 时间小的在前面,时间大的在后面
    28. return (int) (this.time - o.time);
    29. }
    30. }
    31. //自己创建的定时器类
    32. class MyTimer {
    33. //使用 优先级队列的数据结构 来保存若干个任务
    34. //但是调用 schedule 的时候,可能是多个线程调用,并不一定是单线程调用(比如说可以定多个闹钟)
    35. //所以使用 的队列应该是 带有优先级的阻塞队列 PriorityBlockingQueue
    36. private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    37. //command 表示要执行的任务是啥,after 表示多长时间之后来执行这个任务
    38. public void schedule(Runnable command,long after) {
    39. MyTask myTask = new MyTask(command,after);
    40. queue.put(myTask);
    41. }
    42. public MyTimer() {
    43. //在这里启动一个线程,来完成执行任务的工作
    44. //尤其是每次出队列,来执行时间最短的任务
    45. Thread t = new Thread(() -> {
    46. while (true) {
    47. //循环过程中,不断的尝试从队列中获取到队首元素
    48. //判定队首元素当前的时间是否就绪,时间有没有到点,如果就绪就执行,如果不就绪就不执行
    49. try {
    50. MyTask myTask = queue.take();
    51. long curTime = System.currentTimeMillis();
    52. if (myTask.getTime() > curTime) {
    53. //时间还没有到,塞回到队列中
    54. queue.put(myTask);
    55. }else{
    56. //时间到了,直接执行任务
    57. myTask.run();
    58. }
    59. } catch (InterruptedException e) {
    60. e.printStackTrace();
    61. }
    62. }
    63. });
    64. t.start();
    65. }
    66. }

    上面就是定时器的模拟实现代码,现在就需要我们来小小测试一下效果:

    1. //测试代码
    2. public class Demo24 {
    3. public static void main(String[] args) {
    4. MyTimer myTimer = new MyTimer();
    5. myTimer.schedule(new Runnable() {
    6. @Override
    7. public void run() {
    8. System.out.println("2222");
    9. }
    10. },4000);
    11. myTimer.schedule(new Runnable() {
    12. @Override
    13. public void run() {
    14. System.out.println("1111");
    15. }
    16. },2000);
    17. myTimer.schedule(new Runnable() {
    18. @Override
    19. public void run() {
    20. System.out.println("3333");
    21. }
    22. },6000);
    23. }
    24. }

    运行结果: 

    2.3 对自己实现的定时器的进一步优化

    2.3.1 为何需要再进行优化

    这个线程的执行,还是有一定的缺陷的:

    如果队列为空的话,那么的确在调用 take 的时候,在阻塞 没有什么太大的问题~

    如果队列不为空的话,那么 它会执行下面的逻辑:

    1. 取出任务
    2. 比较时间
    3. 如果时间未到,就会插入任务回队列

    但是,当插入任务回队列的时候 且时间未到,就会再次调用 myTask,进入循环~

    但是,由于队列不为空 且时间未到,那么 又是调用,......

    这样的后果,就是 如果队列不为空的话,且如果没有到时间,那么 就会产生大量的循环~

    这就相当于,CPU 在不断的进行 "空转",但是 又没有实质性的任务再执行,相当于在 "盲等"~

    2.3.2 如何进行进一步优化(一)

    我们希望,当我们把任务塞回队列之后,不应该让它立刻循环下一次,应该让线程等一会儿再进入循环~


    如果引入了 sleep方法,看起来是会让线程不盲等,但是会引入新的问题~ 

    举个例子,比如说,定了一个闹钟,想要下午两点和同学一起出去看电影;看了一下时间,现在才是 10:50,于是 sleep 了一个小时,到了 11:50;再看了一下时间,于是又 sleep 了一个小时 ......

    这时候,你突然想起还有一个很紧急的事情没有做 —— 做核酸,在 12:00 之前帮助做核酸的志愿者就要走了~

    但是,在 11:50 ~ 12:50 这段时间中,你仍然在 sleep,就无法在执行这个任务了~

    直到 sleep 结束,这个 "做核酸" 的任务才会被执行(于是,你又要等到下个时间段在去做了)~


    所以说,我们需要想想其它的办法~

    虽然 sleep 不行,但是 wait 可以,相比于 sleep 来说,wait 是可以被提前唤醒的!!!

    如果是能够提前唤醒,就完全可以再插入新任务的时候,把这里的等待给唤醒,再去执行新的任务~

    2.3.3 如何进行进一步优化(二)

    经过上述的优化,已经差不多了,只剩下最后一个小小的问题了~


    有这样一种情况,假设当前的时间是 10:00,此时 取出的队首元素是 11:00 要执行的任务,此时 时间还没有到,于是 就把这个任务塞回到队列中~

    接着 就需要等待 1h,但是,我们知道,多线程执行两个线程的顺序是不确定的,于是 在 put 之后,wait 之前,可能会有另外的新任务到达了,新的任务是在 10:30 运行~

    当我们把 新的任务 插入之后,进行了 notify~

    但是,由于 另外一个线程还没有 wait,所以这个 notify 就相当于空打了一炮~

    于是,回到了wait方法继续执行,就等待了的是 1h,而不是等待了的是 30min~


     之所以出现这个问题,主要是出现了线程调度的问题,因此需要把锁的范围扩大一点就可以了~

    取元素、判断时间、put、wait 应该视为原子操作,如果在其中间插入任务,都可能会出现风险的~ 

     


    当然,如果直接放大锁的范围的话,由于后面的 take 会让线程造成死锁的问题(因为在初始情况下 队列可能是空着的),就会让线程阻塞~ 

    所以还需要一个判断条件:

     

    2.4 附上自己模拟实现定时器的代码

    1. package thread;
    2. import java.util.PriorityQueue;
    3. import java.util.concurrent.PriorityBlockingQueue;
    4. //通过这个类来描述一个任务
    5. class MyTask implements Comparable<MyTask> {
    6. //获取具体任务
    7. private Runnable command;
    8. //执行任务时的时间戳
    9. private long time;
    10. //构造方法
    11. public MyTask (Runnable command,long after) {
    12. this.command = command;
    13. //我们希望知道完整的时间戳(几点几分几秒之后执行),所以用 当前系统时间戳+多长时间后执行 来表示
    14. //此处记录的是绝对时间戳,不是 "多长时间之后执行"
    15. this.time = System.currentTimeMillis() + after;
    16. }
    17. //执行任务的方法,直接在内部调用 Runnable 的 run 即可
    18. public void run() {
    19. command.run();
    20. }
    21. //获取执行时间
    22. public long getTime() {
    23. return time;
    24. }
    25. @Override
    26. public int compareTo(MyTask o) {
    27. //希望 时间小的在前面,时间大的在后面
    28. return (int) (this.time - o.time);
    29. }
    30. }
    31. //自己创建的定时器类
    32. class MyTimer {
    33. //这个是用来阻塞等待的锁对象
    34. private Object locker = new Object();
    35. //使用 优先级队列的数据结构 来保存若干个任务
    36. //但是调用 schedule 的时候,可能是多个线程调用,并不一定是单线程调用(比如说可以定多个闹钟)
    37. //所以使用 的队列应该是 带有优先级的阻塞队列 PriorityBlockingQueue
    38. private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    39. //command 表示要执行的任务是啥,after 表示多长时间之后来执行这个任务
    40. public void schedule(Runnable command,long after) {
    41. MyTask myTask = new MyTask(command,after);
    42. //唤醒
    43. synchronized (locker) {
    44. queue.put(myTask);
    45. locker.notify();
    46. }
    47. }
    48. public MyTimer() {
    49. //在这里启动一个线程,来完成执行任务的工作
    50. //尤其是每次出队列,来执行时间最短的任务
    51. Thread t = new Thread(() -> {
    52. while (true) {
    53. //循环过程中,不断的尝试从队列中获取到队首元素
    54. //判定队首元素当前的时间是否就绪,时间有没有到点,如果就绪就执行,如果不就绪就不执行
    55. try {
    56. synchronized (locker) {
    57. while (queue.isEmpty()) {
    58. locker.wait();
    59. }
    60. MyTask myTask = queue.take();
    61. long curTime = System.currentTimeMillis();
    62. if (myTask.getTime() > curTime) {
    63. //时间还没有到,塞回到队列中
    64. queue.put(myTask);
    65. //比如说,下午两点有新任务,现在 是十一点,那么 一减就可以得出 需要等待 三个小时就可以了
    66. locker.wait(myTask.getTime() - curTime);
    67. }else{
    68. //时间到了,直接执行任务
    69. myTask.run();
    70. }
    71. }
    72. } catch (InterruptedException e) {
    73. e.printStackTrace();
    74. }
    75. }
    76. });
    77. t.start();
    78. }
    79. }
    80. public class Demo24 {
    81. public static void main(String[] args) {
    82. MyTimer myTimer = new MyTimer();
    83. myTimer.schedule(new Runnable() {
    84. @Override
    85. public void run() {
    86. System.out.println("2222");
    87. }
    88. },4000);
    89. myTimer.schedule(new Runnable() {
    90. @Override
    91. public void run() {
    92. System.out.println("1111");
    93. }
    94. },2000);
    95. myTimer.schedule(new Runnable() {
    96. @Override
    97. public void run() {
    98. System.out.println("3333");
    99. }
    100. },6000);
    101. }
    102. }

    好了,关于多线程的第三个案例 —— 定时器就介绍到这里了~

    如果感觉这一篇博客对你有帮助的话,可以一键三连走一波,非常非常感谢啦 ~

  • 相关阅读:
    [b01lers2020]Welcome to Earth-1
    [附源码]计算机毕业设计新能源汽车租赁Springboot程序
    户外耳机品牌哪个好、最新的户外耳机品牌排行
    Linux之进程替换
    不同路径的数目
    uniapp使用uni.downloadFile(OBJECT)结合uni.storage/uni.getstorage实现离线缓存
    系统架构7个非功能性需求
    open62541学习:文件传输
    按钮组件草稿
    oaid SDK 调用问题 F&Q
  • 原文地址:https://blog.csdn.net/qq_53362595/article/details/126416500