软件编程
位置:首页>> 软件编程>> java编程>> 一文带你搞懂Java定时器Timer的使用

一文带你搞懂Java定时器Timer的使用

作者:熬夜磕代码丶  发布时间:2022-09-08 01:18:16 

标签:Java,定时器,Timer

一、定时器是什么

定时器类似于我们生活中的闹钟,可以设定一个时间来提醒我们。

而定时器是指定一个时间去执行一个任务,让程序去代替人工准时操作。

标准库中的定时器: Timer

方法作用
void schedule(TimerTask task, long delay)指定delay时间之后(单位毫秒)执行任务task
public static void main(String[] args) {
       Timer timer = new Timer();
       timer.schedule(new TimerTask() {
           @Override
           public void run() {
               System.out.println("定时器任务! ");
           }
       },1000);
   }

这段程序就是创建一个定时器,然后提交一个1000s后执行的任务。

二、自定义定时器

我们自己实现一个定时器的前提是我们需要弄清楚定时器都有什么:

1.一个扫描线程,负责来判断任务是否到时间需要执行

2.需要有一个数据结构来保存我们定时器中提交的任务

创建一个扫描线程相对比较简单,我们需要确定一个数据结构来保存我们提交的任务,我们提交过来的任务,是由任务和时间组成的,我们需要构建一个Task对象,数据结构我们这里使用优先级队列,因为我们的任务是有时间顺序的,具有一个优先级,并且要保证在多线程下是安全的,所以我们这里使用:PriorityBlockingQueue比较合适。

首先我们构造一个Task对象

class MyTask {
   //即将执行的任务
   private Runnable runnable;
   //在多久后执行
   private long time;

public MyTask(Runnable runnable, long time) {
       this.runnable = runnable;
       this.time = time;
   }

public long getTime() {
       return time;
   }

//执行任务
   public void run() {
       runnable.run();
   }
}

MyTimer类:

public class MyTimer {
   //扫描线程
   private Thread t;
   //创建一个阻塞优先级队列,用来保存提交的Task对象
   private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
   private Object locker = new Object();

//提交任务的方法
   public void schedule(Runnable runnable,long time) {
       //这里我们的时间换算一下,保存实际执行的时间
       MyTask task = new MyTask(runnable,System.currentTimeMillis() +  time);
       queue.put(task);
   }

//构建扫描线程
   public MyTimer() {
       t = new Thread(() -> {
          //我们取出队列中时间最近的元素
           while (true) {
               try {
                   MyTask task = queue.take();
                   long curTime = System.currentTimeMillis();
                   if(curTime < task.getTime()) {
                       //证明还没到执行的时间,再放进队列
                       queue.put(task);
                   } else {
                       //到时间了,执行任务
                       task.run();
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       });
   }
   t.start();
}

虽然我们大体已经写出来了,但是我们这个定时器实现的还有一些问题。

问题1:既然我们是优先级队列,我们再阻塞优先级队列中放入Task对象时,是根据什么建立堆的?

一文带你搞懂Java定时器Timer的使用

我们发现当我们运行程序时,我们的程序也会报这样的错误。

class MyTask implements Comparable<MyTask>{
   //即将执行的任务
   private Runnable runnable;
   //在多久后执行
   private long time;

public MyTask(Runnable runnable, long time) {
       this.runnable = runnable;
       this.time = time;
   }

public long getTime() {
       return time;
   }

//执行任务
   public void run() {
       runnable.run();
   }

@Override
   public int compareTo(MyTask o) {
       return (int) (this.time - o.time);
   }
}

我们需要实现Comparable接口并且重写compareTo方法,指明我们是根据时间来决定在队列中的优先级。

2.我们的扫描线程,扫描的速度太快,造成了不必要的CPU资源浪费。

一文带你搞懂Java定时器Timer的使用

比如我们早上8.00提交了一个中午12.00的任务,那么我们这样的程序就会从8.00一直循环几十亿次,而这样的等待是没有任何意义的。

更合理的方式是,不要在这里忙等,而是&ldquo;阻塞式&rdquo;等待。

public MyTimer() {
       t = new Thread(() -> {
          //我们取出队列中时间最近的元素
           while (true) {
               try {
                   MyTask task = queue.take();
                   long curTime = System.currentTimeMillis();
                   if(curTime < task.getTime()) {
                       //证明还没到执行的时间,再放进队列
                       queue.put(task);
                       synchronized (locker) {
                           locker.wait(task.getTime() - curTime);
                       }
                   } else {
                       //到时间了,执行任务
                       task.run();
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       });
   }
   t.start();

我们重写一下扫描线程,进行修改,当我们判断队列中最近的一个任务的时间都没到时,我们的扫描线程就进行阻塞等待,这里我们使用的不是wait(),而是wait(long time),我们传入的参数是要执行的时间和当前时间的差值,有的同学可能会问了,那这样执行的时候和预期执行的时间不就有出入了嘛?

因为我们程序里的定时操作,本来就难以做到非常准确,因为操作系统调度是随机的,有一定的时间开销,存在ms的误差都是相当正常的,不影响我们的正常使用。

我们上面进行阻塞等待,难道就傻傻的等到时间到了自动唤醒嘛? 有没有啥特殊情况呢?这里是有的,比如我们设定了一个阻塞到12点在唤醒,但我们又提交了一个10点的新任务,那么我们就应该提前唤醒了,所以我们应该在每次提交任务后都进行主动唤醒,再由我们扫描线程决定是执行还是继续阻塞等待。

public void schedule(Runnable runnable,long time) {
       //这里我们的时间换算一下,保存实际执行的时间
       MyTask task = new MyTask(runnable,System.currentTimeMillis() +  time);
       queue.put(task);
       synchronized (locker) {
           locker.notify();
       }
   }

即使我们现在所有正常的情况都考虑到了,但是我们这里仍然存在一种极端的情况。

一文带你搞懂Java定时器Timer的使用

假设我们的扫描线程刚执行完put方法,这个线程就被cpu调度走了,此时我们的另一个线程调用了schedule,添加了新任务,新任务是10点执行,然后notify,因为我们并没有wait(),所以相当于这里是空的notify,然后我们的线程调度回来去执行wait()方法,但是我们的时间差仍然是之前算好的时间差,从8.00点到12.00点,这样就会产生很大的错误。

这里造成这样的问题,是因为我们的take操作和wait操作不是原子的,我们需要在take和wait之间加上锁,保证每次notify的时候,都在wait中。

public MyTimer() {
       t = new Thread(() -> {
           while (true) {
               try {
                   synchronized (locker) {
                       MyTask Task = queue.take();
                       long curTime = System.currentTimeMillis();
                       if (curTime < Task.getTime()) {
                           queue.put(Task);
                           locker.wait(Task.getTime() - curTime);
                       } else {
                           Task.run();
                       }
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       });
       t.start();

一文带你搞懂Java定时器Timer的使用

来源:https://blog.csdn.net/buhuisuanfa/article/details/128582096

0
投稿

猜你喜欢

手机版 软件编程 asp之家 www.aspxhome.com