当前位置:   article > 正文

【多线程案例】Java实现简单定时器(Timer)_使用线程实现一个简单的计时器

使用线程实现一个简单的计时器

1.定时器(Timer)

1.什么是定时器?

在日常生活中,如果我们想要在 t 时间 后去做一件重要的事情,那么为了防止忘记,我们就可以使用闹钟计时器功能,它会在 t 时间后执行任务(响铃)提醒我们去执行这件事情. — 这就是Java定时器的简单功能。它作为一种日常开发组件。约定一个时间,时间到达之后,执行某个任务。常被用于网络通信。

也比如在客户端和服务器之间,当客户端发出去请求之后,服务器就要返回响应,客户端这边要等待响应,而网络环境是复杂的,如果等待时间较长,这个原因是啥,是请求没法送过去?响应丢了?还是服务器出问题了。对于客户端来说,不能无限的等,需要先设置一个最大的期限。这时"等待最大期限"就可以通过定时器的方式实现了。

2. Java内置定时器的常用功能

Java中的定时器的类是 : Timer ,为util包中的一个无继承关系的类, 从该类的构造方法中,我们可以使用无参构造器创建该类的对象,也可以在创建类对象的时候指定定时器中所需要的线程的名字,与是否为守护进程。

  • 在定时器中最常用的方法就是 schedule(TimerTask task, long delay)
  • 该方法传参的是一个 TimerTask 对象,与定时器约定的执行时间间隔 delay
  • 包:import java.util.Timer;

 而 TimerTask 类则是一个来描述计时器任务的类,该类中有 抽象方法 run(),并且该类是实现了runnable接口的,所以我们给 schedule 传参中的 TimerTask 对象都要重写 run() 方法,重写的run() 方法中的语句,则是定时器需要执行的语句。

而 delay 则是我们约定从当前时间后的 delay 内执行传入的任务.时间单位为 毫秒。 

 接下来我们来看一个简单的定时器的使用 :

我们在创建定时器的时候指定了定时器中的扫描线程的线程名,然后使用 schedule 方法传入任务与任务执行的间隔时间 1000 毫秒
这个时候在执行该代码的1000毫秒后,定时器就会将该任务执行。

  1. import java.util.Timer;
  2. import java.util.TimerTask;
  3. public class demo1 {
  4. public static void main(String[] args) {
  5. Timer timer = new Timer("线程1");
  6. timer.schedule(new TimerTask() { //使用匿名内部类继承TImerTask类
  7. @Override
  8. public void run() { //TimerTask类实现了Runnable接口要重写run方法
  9. System.out.println("执行任务");
  10. }
  11. },1000); //delay相对时间 任务执行时间
  12. System.out.println("程序启动!");
  13. }
  14. }

主线程执行schedule方法的时候,就是把这个任务放到timer对象中了。并且timer里面也包含一个线程(扫描线程),时间一到,扫描线程就会执行刚才安排的任务了。

可以发现,程序运行完,整个程序并没有结束。正是因为TImer里的线程,阻止了线程结束!

利用 jconsole 观察该线程处于 WAITING 状态:

3.自定义定时器


3.1 实现定时器思路

  1. 实现定时器,我们首先需要有一个可以来描述定时器中的任务的类 MyTimerTask
  2. 需要使用一个数据结构将定时器中的任务按照执行时间的顺序给组织起来。
  3. 在 MyTimer 定时器类中会有一个线程不断地去访问定时器的任务,查看是否到了指定执行时间。
     

3.2 MyTimerTask 类:

  1. //描述一个任务的类
  2. public class MyTimerTask implements Comparable<MyTimerTask>{
  3. //要有一个任务
  4. private Runnable runnable;
  5. //要有一个时间
  6. private long time;
  7. //构造方法 传入任务和时间
  8. public MyTimerTask(Runnable runnable,long delay) {
  9. //任务
  10. this.runnable = runnable;
  11. //任务发生时间
  12. this.time = System.currentTimeMillis()+delay;
  13. }
  14. //为外部提供获取任务发生时间
  15. public long getTaskTime() {
  16. return this.time;
  17. }
  18. //为外部提供获取任务
  19. public Runnable getRunnable() {
  20. return this.runnable;
  21. }
  22. //重写比较方法
  23. @Override
  24. public int compareTo(MyTimerTask o) {
  25. return (int)(this.time-o.time);
  26. }
  27. }

实现Comparable接口是因为数据结构我们用到了优先级队列,需要重写比较方法,重新定义比较规则。有两种方法,一种是实现Comparable接口,另一种是比较器Comparator接口,用内部类实现。

3.3 MyTimer 类:

  1. import java.util.PriorityQueue;
  2. //定时器 即指定几分钟后或其他时间后干什么
  3. //定时器
  4. public class MyTimer {
  5. //优先级队列 使用比较器 匿名内部类
  6. //private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(new Comparator<MyTimerTask>() {
  7. // @Override
  8. // public int compare(MyTimerTask o1, MyTimerTask o2) {
  9. // return (int) (o1.getTaskTime()-o2.getTaskTime());
  10. // }
  11. //});
  12. //优先级队列
  13. private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
  14. //多个线程针对同一个对象上锁 锁对象
  15. private Object locker = new Object();
  16. public void schedule(Runnable runnable,long delay) {
  17. //线程不安全
  18. synchronized (locker) {
  19. //添加任务及任务时间
  20. queue.offer(new MyTimerTask(runnable,delay));
  21. //唤醒队列
  22. locker.notify();
  23. }
  24. }
  25. //扫描线程
  26. public MyTimer() {
  27. //创建一个扫描线程
  28. Thread t1 = new Thread(()->{
  29. //不停地扫描队列 即队头 查看是否到达时间
  30. //有可能下一次新添加的任务的时间更短 队头改变
  31. while(true) {
  32. synchronized (locker) {
  33. while (queue.isEmpty()) {
  34. try {
  35. //队列为空 等待
  36. locker.wait();
  37. } catch (InterruptedException e) {
  38. e.printStackTrace();
  39. }
  40. }
  41. //队列不为空 有任务 下面进行时间比较
  42. //获取当前任务
  43. MyTimerTask task = queue.peek();
  44. //获取当前时间 时间戳
  45. long currTime = System.currentTimeMillis();
  46. if(currTime>=task.getTaskTime()) {
  47. //到任务时间 执行任务
  48. task.getRunnable().run();
  49. queue.poll();
  50. }else{
  51. //未到任务时间 也进行等待 降低扫描速度
  52. try {
  53. locker.wait(task.getTaskTime()-currTime);
  54. } catch (InterruptedException e) {
  55. e.printStackTrace();
  56. }
  57. }
  58. }
  59. }
  60. });
  61. //启动线程
  62. t1.start();
  63. }
  64. }

数据结构我们选择使用优先级队列,有啥好处?假如在选择数据结构之前,我们先假设使用数组ArrayList,此时扫描线程,就需要不停地遍历数组中的每个任务,判定每个任务是否到达执行时间。这样的遍历效率是非常低的。如果使用优先级队列,再重写比较方法,让整个任务由时间大小按照小根堆排列,那么最先执行的就是时间最小的任务了,时间复杂度将降到O(1),判定任务时间是否到达更高效。

在该类的构造方法中,我们还创建了一个线程,不断地对任务队列中优先级最高(最快执行)的任务进行查看, 看是否到达执行时间。

当队列为空时,线程进入阻塞等待,直到添加一个任务时,线程继续执行。

队列中有任务时,但当前时间最短的任务还未到达执行时间时,也进行阻塞等待。这里的阻塞等待是有参的,为执行时间与当前时间的差值。其实也完全可以不等待,继续循环扫描。此处阻塞的好处就是wait之后,就会释放锁,线程就不会在CPU上执行了,就可以把CPU资源让给其他线程使用了。

那么对于wait和sleep来说都是等待,为啥不用sleep?sleep是指定时间让线程进行休眠,假如在sleep的过程中,我添加了一个比之前队列中任务执行时间还早的任务,那么sleep就不能及时执行最新的这个任务。而我设计的代码中,若使用wait,每添加一个任务时,notify都会唤醒不管是因为空队列进入阻塞状态的线程,或者是因为未到达任务时间而阻塞等待的线程(两种阻塞不会同时出现),就算是添加了一个比之前队列中任务执行时间还早的任务,也能及时执行任务。

3.4 测试

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

结果: 

3.5 总结

三个注意点:

  1. 任务类,可比较的问题。
  2. 线程安全问题。
  3. 忙等问题。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/495569
推荐阅读
相关标签
  

闽ICP备14008679号