天天看点

Java中的定时器:java.util.Timer

作者:IT技术控

1.定时器

1.1 含义

  在Java中,定时器(Timer)是一个工具类,用于安排任务(Task)在指定时间后执行或以指定的时间间隔重复执行。它可以用于执行定时任务、定时调度和时间延迟等操作。

定时器(Timer)可以应用于许多场景,比如:

  1. 调度任务:当你需要按照预定时间执行任务时,可以使用定时器。例如,每天凌晨执行数据备份、定时生成报表、定时发送通知等。
  2. 超时处理:当你需要处理某个操作的超时情况时,可以使用定时器。例如,设置一个操作的超时时间,如果在规定时间内未完成,则执行相应的超时处理逻辑。

1.2 标准库中的定时器

  Java中的定时器:java.util.Timer,它的常用方法:

方法 描述
schedule(TimerTask task, Date time) 安排在指定时间执行任务。
schedule(TimerTask task, long delay) 安排在指定延迟时间后执行任务。
schedule(TimerTask task, long delay, long period) 安排在指定延迟时间后以指定的时间间隔重复执行任务。
scheduleAtFixedRate(TimerTask task, Date firstTime, long period) 安排在指定时间开始以固定的时间间隔重复执行任务。
scheduleAtFixedRate(TimerTask task, long delay, long period) 安排在指定延迟时间后以固定的时间间隔重复执行任务。
cancel() 取消定时器的所有任务。
purge() 从定时器的任务队列中删除所有已取消的任务。
java复制代码public class Main {
    public static void main(String[] args) {
        
        Timer timer = new Timer();
        //调度指定的任务在指定的延迟时间(3000ms)后执行。
        timer.schedule(new TimerTask() {
            //待执行的任务
            @Override
            public void run() {
                System.out.println("hello");
            }
        },3000);
        
    }
}
           

也可以一次注册多个任务:

java复制代码public class Main {
    public static void main(String[] args) {

        Timer timer = new Timer();
        //在指定的延迟时间(1000ms)后执行。
        timer.schedule(new TimerTask() {
            //待执行的任务
            @Override
            public void run() {
                System.out.println("任务1");
            }
        },1000);

        //在指定的延迟时间(2000ms)后执行。
        timer.schedule(new TimerTask() {
            //待执行的任务
            @Override
            public void run() {
                System.out.println("任务2");
            }
        },2000);

        //在指定的延迟时间(3000ms)后执行。
        timer.schedule(new TimerTask() {
            //待执行的任务
            @Override
            public void run() {
                System.out.println("任务3");
            }
        },3000);
    }
}
           

2.简单模拟实现定时器

2.1 实现思路

  1. 使用一个数据结构来保存所有的任务,这些任务是根据时间的大小来进行先后执行的,所以这里使用优先级队列。由于这里是多线程的环境,所以这里采用PriorityBlockingQueue(优先级阻塞队列),时间越小优先级越高。
  2. 我们需要使用一个线程来扫描定时器里面的任务是否到达执行时间,由于我们采用的是优先级队列数据结构,所以只需扫描队首元素。如果队首还没到执行时间,那么后面的元素不可能到达执行时间。
  3. 任务用一个类MyTask来表示,这里需要实现Comparable接口,因为它需要存入优先级队列。其中的属性:
java复制代码//表示定时器中的任务
class MyTask implements Comparable<MyTask>{
    //要执行的任务内容
    private Runnable runnable;

    //延迟时间
    private long time;

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


    //为了便于后面的比较,需要提供 get 方法
    public long getTime() {
        return time;
    }

    //表示任务开始执行
    public void run(){
        this.runnable.run();
    }
    
    @Override
    public int compareTo(MyTask o) {
        return (int)(this.getTime() - o.getTime());
    }
}
           
  1. 实现添加任务的方法schedule:
java复制代码public class MyTimer {

    //扫描线程
    private Thread thread;

    //优先级队列(这里为阻塞队列)
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    /**
     * 这个方法是用来注册(添加)任务的
     * @param runnable 表示待执行的任务
     * @param after 表示多少时间过后执行任务
     */
    public void schedule(Runnable runnable,long after){
        //添加任务,注意这里的时间是 System.currentTimeMillis() + after
        MyTask task = new MyTask(runnable,System.currentTimeMillis() + after);
        queue.put(task);
    }
}
           
  1. 添加一个线程来检测队首元素:
java复制代码    //当创建对象的时候就直接开启一个线程
	public MyTimer(){
        thread = new Thread(()->{
           while(true){
               //取出队首,如果到时间了就执行。
               try {
                   MyTask myTask = queue.take();
                   long curTime = System.currentTimeMillis();
                   if(curTime < myTask.getTime()){
                       //时间未到,不执行
                       queue.put(myTask);
                   }else {
                       //时间已到,执行
                       myTask.run();
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        thread.start();
    }
           

  就这样就完了吗?其实不然,在上面代码中while (true)转的太快了, 造成了无意义的 CPU 浪费,如果第一个任务设定的是 1 min 之后执行某个逻辑,那么在这一分钟内 CPU 会一直存取队首元素。所以这里需要借助该对象的wait / notify来解决 while (true) 的忙等问题。

java复制代码    public MyTimer(){
        thread = new Thread(()->{
           while(true){
               //取出队首,如果到时间了就执行。
               try {
                   MyTask myTask = queue.take();
                   long curTime = System.currentTimeMillis();
                   if(curTime < myTask.getTime()){
                       queue.put(myTask);
                       //时间未到,不执行,这里的 this 表示 MyTimer 对象
                       synchronized (this){
                           //阻塞一段时间
                           this.wait(myTask.getTime() - curTime);
                       }
                   }else {
                       //时间已到,执行
                       myTask.run();
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        thread.start();
    }


    /**
     * 这个方法是用来注册(添加)任务的
     * @param runnable 表示待执行的任务
     * @param after 表示多少时间过后执行任务
     */
    public void schedule(Runnable runnable,long after){
        //添加任务,注意这里的时间是 System.currentTimeMillis() + after
        MyTask task = new MyTask(runnable,System.currentTimeMillis() + after);
        queue.put(task);
        synchronized(this){
            this.notify();
        }
    }
           

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

  还没结束!上面的代码还是有缺陷的。假设当 thread 线程执行完 queue.take() 过后,myTask.getTime() - curTime 的值为 1 个小时。这时 CPU 调度了其它线程(假设为 t2) 执行, t2 线程调用 schedule 方法,延时时间为 30 分钟,并调用 put 方法,随后再执行 notify 方法。然而这时 wait 方法还没有执行,notify 相当于失效了。这时CPU再调度 thread 线程执行,但是 myTask.getTime() - curTime 的值本应是 30 分钟(新添加了一个任务),但是实际上却是 1 个小时。   这是因为queue.take()与wait不是原子操作,所以才导致这个问题的发生,下面是改进后的代码。

java复制代码    public MyTimer(){
        thread = new Thread(()->{
           while(true){
               //取出队首,如果到时间了就执行。
               try {
                   synchronized (this){
                       MyTask myTask = queue.take();
                       long curTime = System.currentTimeMillis();
                       if(curTime < myTask.getTime()){
                           queue.put(myTask);
                           //时间未到,不执行
                           //阻塞一段时间
                           this.wait(myTask.getTime() - curTime);
                       }else {
                           //时间已到,执行
                           myTask.run();
                       }                       
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        thread.start();
    }
           

2.2 完整代码

java复制代码//表示定时器中的任务
class MyTask implements Comparable<MyTask>{
    //要执行的任务内容
    private Runnable runnable;

    //延迟时间
    private long time;

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


    //为了便于后面的比较,需要提供 get 方法
    public long getTime() {
        return time;
    }

    //表示任务开始执行
    public void run(){
        this.runnable.run();
    }

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

public class MyTimer {

    //扫描线程
    private Thread thread;

    //优先级队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    public MyTimer(){
        thread = new Thread(()->{
           while(true){
               //取出队首,如果到时间了就执行。
               try {
                   synchronized (this){
                       MyTask myTask = queue.take();
                       long curTime = System.currentTimeMillis();
                       if(curTime < myTask.getTime()){
                           queue.put(myTask);
                           //时间未到,不执行
                           //阻塞一段时间
                           this.wait(myTask.getTime() - curTime);
                       }else {
                           //时间已到,执行
                           myTask.run();
                       }
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        thread.start();
    }


    /**
     * 这个方法是用来注册(添加)任务的
     * @param runnable 表示待执行的任务
     * @param after 表示多少时间过后执行任务
     */
    public void schedule(Runnable runnable,long after){
        //添加任务,注意这里的时间是 System.currentTimeMillis() + after
        MyTask task = new MyTask(runnable,System.currentTimeMillis() + after);
        queue.put(task);
        synchronized(this){
            this.notify();
        }
    }
}