• 【多线程案例】定时器


    1. 定时器是什么?

    定时器也是软件开发中的一个重要组件. 类似于一个 "闹钟". 达到一个设定的时间之后, 就执行某个指定好的代码.

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

    2. 使用标准库中的定时器

    • 标准库中提供了一个 Timer 类(java.util包下面). Timer 类的核心方法为 schedule .
    • schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后 执行 (单位为毫秒).
    1. public static void main(String[] args) {
    2. Timer timer = new Timer();
    3. timer.schedule(new TimerTask() {
    4. @Override
    5. public void run() {
    6. System.out.println("hello");
    7. }
    8. }, 3000);
    9. }

    其中TimerTask()就是一个实现了Runnable的抽象类:

    可以把它的作用看成是给定时器一个任务,而第二个参数就是指定多久时间后执行这个任务。

    3. 手写代码实现定时器

    思考一下定时器的构成需要哪些?

    • 一个带有优先级的阻塞式队列
    • 队列中的每一个元素都是一个“任务”对象
    • “任务”对象中包含两个属性,一个属性用于描述任务,也就是一个Runnable,另一个属性用来定义delay。如此一来对手元素就是最即将要执行的任务。
    • 同时需要有一个线程不停的扫描队首元素。看队首元素是否到了执行时间。

    1)写一个任务类,任务类还必须能够按照时间来比大小,因为优先级阻塞队列需要比较大小

    1. //任务类 描述任务和任务的delay时间
    2. static class Task implements Comparable{
    3. //任务
    4. private Runnable command;
    5. //delay
    6. private long time;
    7. public Task(Runnable command,long time){
    8. this.command = command;
    9. //时间是在现在的时间的基础上加上delay
    10. this.time = System.currentTimeMillis() + time;
    11. }
    12. public void run(){
    13. command.run();
    14. }
    15. @Override
    16. public int compareTo(Task o) {
    17. return (int)(this.time - o.time);
    18. }
    19. }

     2)需要有一个优先级阻塞队列来存放用户注册的任务

    1. //优先级阻塞队列 核心结构
    2. //队首存放的是最近要执行的任务 time最小
    3. private PriorityBlockingQueue queue = new PriorityBlockingQueue();
    4. public void schedule(Runnable command,long time){
    5. //生成一个任务 然后放进去优先级队列
    6. Task task = new Task(command, time);
    7. queue.put(task);
    8. }

    3)在构造方法中整一个线程对队首元素扫描,看是否到了执行时间

    1. public MyTimer(){
    2. //在构造方法中来一个扫面线程 一直扫描队首的元素是否到了执行时间
    3. Thread work = new Thread(() -> {
    4. while (true) {
    5. try {
    6. Task task = queue.take();
    7. long curTime = System.currentTimeMillis();
    8. if (task.time > curTime) {
    9. // 时间还没到, 就把任务再塞回去
    10. queue.put(task);
    11. } else {
    12. // 时间到了, 可以执行任务
    13. task.run();
    14. }
    15. } catch (InterruptedException e) {
    16. e.printStackTrace();
    17. break;
    18. }
    19. }
    20. });
    21. work.start();
    22. }

    用写一个测试:

    1. //测试代码
    2. public static void main(String[] args) {
    3. MyTimer myTimer = new MyTimer();
    4. myTimer.schedule(new Runnable() {
    5. @Override
    6. public void run() {
    7. System.out.println("我有第一个任务!");
    8. }
    9. },3000);
    10. myTimer.schedule(new Runnable() {
    11. @Override
    12. public void run() {
    13. System.out.println("我有第二个任务!");
    14. }
    15. },1000);
    16. myTimer.schedule(new Runnable() {
    17. @Override
    18. public void run() {
    19. System.out.println("我有第三个任务!");
    20. }
    21. },2000);
    22. }

    此时已经可以按照定时器的工作原理来完成任务了:

     但是当前的代码还存在着比较严重的问题,就是在3)中如果时间没有到的话会存在cpu一直比较的情况。举个例子,比如小明九点上班,他七点在床上突然醒了。正常情况下应该是继续睡睡到平时订的闹钟时间,但是如果小明一直看表一直看表知道闹铃响起,这样既没有休息也没有做有意义的事情,是十分愚蠢的行为。代码的问题也就在于此,如果没有到执行时间,不管还有多久还都会一直比较有没有到执行时间是没有意义的,也就是处于”忙等“状态。

    优化的话,应该让系统在看到当前队首任务还没有到达执行时间的时候就执行wait(时间差)。但是此时还存在另外一个问题,系统wait一段时候之后确实会执行队首的任务,但是如果在wait的时间中又来了新的任务并且新的任务重新处于了队首,此时就会出bug了。正确的做法是在每次有新的任务被注册的时候都通知一下结束wait。

    修改代码:

    1.引入一个lock对象,借助该对象的wait/notify来解决忙等状态

    private Object lock = new Object();

    2.修改构造方法中的work的工作方法

    1. public MyTimer(){
    2. //在构造方法中来一个扫面线程 一直扫描队首的元素是否到了执行时间
    3. Thread work = new Thread(() -> {
    4. while (true) {
    5. try {
    6. Task task = queue.take();
    7. long curTime = System.currentTimeMillis();
    8. if (task.time > curTime) {
    9. // 时间还没到, 就把任务再塞回去
    10. queue.put(task);
    11. // 等待一段时间
    12. synchronized (lock){
    13. lock.wait(task.time - curTime);
    14. }
    15. } else {
    16. // 时间到了, 可以执行任务
    17. task.run();
    18. }
    19. } catch (InterruptedException e) {
    20. e.printStackTrace();
    21. break;
    22. }
    23. }
    24. });
    25. work.start();
    26. }

    3. 修改 Timer 的 schedule 方法, 每次有新任务到来的时候唤醒一下 worker 线程. (因为新插入的任务可能是需要马上执行的).

    1. public void schedule(Runnable command,long time){
    2. //生成一个任务 然后放进去优先级队列
    3. Task task = new Task(command, time);
    4. queue.put(task);
    5. //有新任务来了 唤醒work 检测是否有更新的工作需要执行
    6. synchronized (lock){
    7. lock.notify();
    8. }
    9. }

    完整代码:

    1. public class MyTimer {
    2. //任务类 描述任务和任务的delay时间
    3. static class Task implements Comparable{
    4. //任务
    5. private Runnable command;
    6. //delay
    7. private long time;
    8. public Task(Runnable command,long time){
    9. this.command = command;
    10. //时间是在现在的时间的基础上加上delay
    11. this.time = System.currentTimeMillis() + time;
    12. }
    13. public void run(){
    14. command.run();
    15. }
    16. @Override
    17. public int compareTo(Task o) {
    18. return (int)(this.time - o.time);
    19. }
    20. }
    21. //优先级阻塞队列 核心结构
    22. //队首存放的是最近要执行的任务 time最小
    23. private PriorityBlockingQueue queue = new PriorityBlockingQueue();
    24. public void schedule(Runnable command,long time){
    25. //生成一个任务 然后放进去优先级队列
    26. Task task = new Task(command, time);
    27. queue.put(task);
    28. //有新任务来了 唤醒work 检测是否有更新的工作需要执行
    29. synchronized (lock){
    30. lock.notify();
    31. }
    32. }
    33. private Object lock = new Object();
    34. public MyTimer(){
    35. //在构造方法中来一个扫面线程 一直扫描队首的元素是否到了执行时间
    36. Thread work = new Thread(() -> {
    37. while (true) {
    38. try {
    39. Task task = queue.take();
    40. long curTime = System.currentTimeMillis();
    41. if (task.time > curTime) {
    42. // 时间还没到, 就把任务再塞回去
    43. queue.put(task);
    44. // 等待一段时间
    45. synchronized (lock){
    46. lock.wait(task.time - curTime);
    47. }
    48. } else {
    49. // 时间到了, 可以执行任务
    50. task.run();
    51. }
    52. } catch (InterruptedException e) {
    53. e.printStackTrace();
    54. break;
    55. }
    56. }
    57. });
    58. work.start();
    59. }
    60. //测试代码
    61. public static void main(String[] args) {
    62. MyTimer myTimer = new MyTimer();
    63. myTimer.schedule(new Runnable() {
    64. @Override
    65. public void run() {
    66. System.out.println("我有第一个任务!");
    67. }
    68. },3000);
    69. myTimer.schedule(new Runnable() {
    70. @Override
    71. public void run() {
    72. System.out.println("我有第二个任务!");
    73. }
    74. },1000);
    75. myTimer.schedule(new Runnable() {
    76. @Override
    77. public void run() {
    78. System.out.println("我有第三个任务!");
    79. }
    80. },2000);
    81. }
    82. }

    此时代码还有问题吗????

    理论上说说代码中还是有一点小问题的。(烧脑啊.....)上图:

    了解了上述问题之后,就不难发现,问题出现的原因,是因为当前 take 操作,和 wait 操作,并非是原子的如果在 take 和 wait 之间加上锁,保证在这个过程中,不会有新的任务过来,问题自然解决(换句话说只要保证每次 notify 时确实都正在 wait )

  • 相关阅读:
    C語言基礎联系
    Java Spring Boot 写 API 接口
    MarkDown语法超详细讲解
    docker 启动容器
    Nginx原理以及基础知识详解
    Java面试题以及答案--SpringBoot
    小程序类找茬游戏开发:创造富有挑战性和娱乐性的游戏体验
    nacos 注解
    java计算机毕业设计Web前端开发技术儿童教育网站MyBatis+系统+LW文档+源码+调试部署
    执行上下文,js、React、HTML中的this
  • 原文地址:https://blog.csdn.net/qq_45875349/article/details/132917641