javaEE 初阶 — 定时器

定时器

1 什么是定时器


定时器 类似于一个 “闹钟”,达到一个设定的时间之后,就执行某个指定好的代码。

定时器是一种实际开发中非常常用的组件。

比如网络通信中,如果对方 500ms 内没有返回数据,则断开连接尝试重连。
比如一个 Map,希望里面的某个 key 在 3s 之后过期(自动删除)。

类似于这样的场景就需要用到定时器。

2 标准库中定时器


Timer 这个类就是标准库的定时器

 Timer timer = new Timer();

定时器使用

package thread;

import java.util.Timer;
import java.util.TimerTask;

public class ThreadDemo4 {

    public static void main(String[] args) {
        // Timer 这个类就是标准库的定时器
        System.out.println("程序启动!");
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定时器任务启动");
            }
        },5000); //5000毫秒后执行 run 方法中的任务
    }
}




在等待了5000毫秒后,就执行了定时器的任务。

schedule 这个方法的效果是给定时器注册一个任务。
但是这个任务不会立即执行,而是在指定时间进行执行。

3 实现一个定时器

3.1 实现的思路


1、让被注册的任务能够在指定时间被执行

  • 单独在定时器内部搞个线程,让这个线程周期性的扫描,判定任务是否到时间了。
  • 如果到时间了,就执行;没到时间就等等。

2、一个定时器是可以注册多个任务的,这多个任务会按照约定时间按顺序执行

  • 这里的多个任务需要使用 优先级队列 来保存。

3.2 为什么要使用优先级队列来保存任务


定时器里的每一个任务都是带有 “时间” 概念的,也就是多长时间过后就执行。
可以肯定的是,时间越靠前的越先执行。

可以把时间小的,作为优先级最高。
此时的队首元素就是整个队列中最要先执行的任务。

此时只需要扫描线程扫描队首元素即可,而不必遍历整个队列。
因为如果队首元素还没到执行的时间,后续的元素就更不可能到执行的时间。

3.3 开始实现


1、我们可以使用标准库中带有阻塞功能的优先级队列: PriorityBlockingQueue 来保存
要执行的任务。

定义一个类来表示我们要执行的任务和执行的时间

//表示定时器中的任务
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();
    }
}


此时 MyTask 就是要保存在 PriorityBlockingQueue 中的任务

class MyTimer {
    //扫描线程
    private Thread search = null;

    //保存任务的阻塞优先级队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
}


2、定时器类需要注册一个 “schedule” 方法来注册任务

我们期望这里保存的是一个 绝对时间,而 after 是一个像 1000ms 这样的毫秒级时间,
一个时间间隔。

所以需要使用当前的时间戳加上 System.currentTimeMillis() 得到一个是在什么时间去执行的标准时间戳。

//第一个参数是任务内容
//第二个参数是任务在多少毫秒之后执行
public void schedule(Runnable runnable, Long after) {
    //注意这里的时间换算
    MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);
    queue.put(task); //填到队列当中
}


3、如何实现扫描线程的主要逻辑

1、因为使用的是 优先级队列,所以这里只要取出队首元素即可。

  MyTask myTask = queue.take();


2、计算出当前的时间

 Long curTime = System.currentTimeMillis();


3、如果到了执行任务的时间就执行,没到就把任务重新塞回队列中

 if (curTime < myTask.getTime()) {
     // 要把任务塞回到队列中
     queue.put(myTask);
 } else { // 到执行任务时间了
     // 执行任务
     myTask.run();
 }


完整代码

 //构造方法里创建一个线程
 public MyTimer() {
     search = 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();
             }
         }
     });
     search.start();
 }


上述代码存在的两个问题

1、没有指定 MyTask 怎么比较优先级

现在执行两个任务看一下状况

 public static void main(String[] args) throws InterruptedException{
     MyTimer myTimer = new MyTimer();
     myTimer.schedule(new Runnable() {
         @Override
         public void run() {
             System.out.println("任务1");
         }
     }, 1000);

     myTimer.schedule(new Runnable() {
         @Override
         public void run() {
             System.out.println("任务2");
         }
     }, 2000);
 }




Comparable 用来描述比较规则的接口,这里提示我们还没有描述规则的 Comparable 接口。

可以让 MyTask 类实现 Comparable 接口
或者也可以使用 Comparable 单独写一 个比较器

下面是实现一个 Comparable

class MyTask implements Comparable<MyTask> {
    @Override
    public int compareTo(MyTask o) {
        // 这里会返回 <0 >0 =0 三种结果
        // this 比 o 小 返回 <0
        // this 比 o 大 返回 >0
        // this 等于 o 返回 =0
        return (int) (this.time - o.time); // 因为时间是long类型的,所以返回需要强制类型转换
    }
}




2、如果执行的时间没到就会一直重复取出来塞进去的操作(忙等

按理说,等待是要释放 CPU 资源的,让 CPU 资源可以干别的事情。
但是忙等,即进行了等待,又占用着 CPU 资源。

就像是有的人虽然今天休假,但是一会又要线上开会,一会又要打扫卫生。
自己还没怎么休息,但是一天就过去了,自己虽然是在休假,但是也没有闲着。

如果此时还没到任务执行的时间,比如说任务执行的时间是 14:00, 但是现在是 13:00
那么在这个时间段内,上述代码的循环操作就可能会被执行数十亿次,甚至更多。

就好比 18:00 就下课了,但是此时是 17:30 ,我过一会看一下时间,过一会看一下时间。
虽然是在等待着下课时间的到来,但是我也没有闲着。


针对上述的情况,不要在忙等了,而是要进行阻塞式等待。
可以使用 sleep 或者 wait

不使用 sleep 的原因:

  • 随时都有可能有新的任务到来,如果新任务执行的时间更早呢。
    也就是说这里等待的时间不明确。

  • 如果新的任务执行的时间是 30 分钟后,但是 sleep 设置的时间是 1个小时,
    那么这个时候就会错过这个任务。

使用 wait 更合适,更方便随时唤醒。
如果有新的任务来了就 notify 唤醒,然后在检查一下时间,重新计算要等待的时间。
而且 wait 也提供了一个带有 “超时时间” 的版本

带有超时时间的 wait 就可以保证:

  • 当新任务来的时候,随时 notify 唤醒
  • 如果没有新任务,则最多等到之前旧任务中的最早任务时间到,就会被唤醒。

在 put 操作之后 进行 wait,还要搭配锁来使用。

在 schedule 方法里进行唤醒(notify)

synchronized (this) {
    this.wait(myTask.getTime() - curTime); // 得到需要等待的时间
}

// 唤醒wait
synchronized (this) {
    this.notify();
}


此时的代码还有一个和线程随机调度相关的问题

假设代码执行到了 ** queue.put(myTask);** ,这个线程就要从 cpu 调度走了。
当线程回来之后,接下来就要进行 wait 操作了,此时 wait 的时间已经是算好的。

比如当前时间是 13:00 ,任务时间是 14:00 ,即将要 wait 1 小时。(此时还没有执行wait)
如果此时有另一个线程调用了 schedule 方法添加新任务,新任务是 13:30 执行。

由于 扫描线程 wait 还没执行呢,所以此处的 notify 只是会空打一炮,
不会产生任何的唤醒操作。

此时此刻,新的任务虽然已经插入到队列,新的任务也是在队首,
紧接着,扫描线程回到 cpu 了,此时等待的时间仍然是 1 小时。

因此,13:30 的任务就被错过了。


了解了上述问题之后就不难发现,问题出现的原因,是因为当前 take 操作和 wait 操作不是原子的。

如果在 take 和 wait 之间加上锁,保证在这个过程中不会有新的任务过来,问题自然解决。

 //构造方法里创建一个线程
 public MyTimer() {
     search = new Thread(() -> {
         while (true) {
             try {
                 // 取出队首元素,检查队首元素任务是否到时间了
                 // 如果没到时间,就把任务重新放到队列中
                 // 如果到时间了,就执行任务
                 synchronized (this) {
                     MyTask myTask = queue.take(); //拿出队首元素
                     long curTime = System.currentTimeMillis(); //计算当前的时间
                     // 还没到执行的时间
                     if (curTime < myTask.getTime()) {
                         // 要把任务塞回到队列中
                         queue.put(myTask);
                         // put 之后进行 wait
                         this.wait(myTask.getTime() - curTime); // 得到需要等待的时间
                     } else { // 到执行任务时间了
                         // 执行任务
                         myTask.run();
                     }
                 }
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     });
     search.start();
 }

完整代码

package thread;

import java.util.concurrent.PriorityBlockingQueue;

//表示定时器中的任务
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) {
        // 这里会返回 <0 >0 =0 三种结果
        // this 比 o 小 返回 <0
        // this 比 o 大 返回 >0
        // this 等于 o 返回 =0
        return (int) (this.time - o.time); // 因为时间是long类型的,所以返回需要强制类型转换
    }
}

class MyTimer {
    //扫描线程
    private Thread search = null;

    //保存任务的阻塞优先级队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    //构造方法里创建一个线程
    public MyTimer() {
        search = new Thread(() -> {
            while (true) {
                try {
                    // 取出队首元素,检查队首元素任务是否到时间了
                    // 如果没到时间,就把任务重新放到队列中
                    // 如果到时间了,就执行任务
                    synchronized (this) {
                        MyTask myTask = queue.take(); //拿出队首元素
                        long curTime = System.currentTimeMillis(); //计算当前的时间
                        // 还没到执行的时间
                        if (curTime < myTask.getTime()) {
                            // 要把任务塞回到队列中
                            queue.put(myTask);
                            // put 之后进行 wait
                            this.wait(myTask.getTime() - curTime); // 得到需要等待的时间
                        } else { // 到执行任务时间了
                            // 执行任务
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        search.start();
    }

    //第一个参数是任务内容
    //第二个参数是任务在多少毫秒之后执行
    public void schedule(Runnable runnable, long after) {
        //注意这里的时间换算
        MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);
        queue.put(task); //填到队列当中

        // 唤醒wait
        synchronized (this) {
            this.notify();
        }
    }
}

public class ThreadDemo5 {

    public static void main(String[] args) throws InterruptedException {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1");
            }
        }, 1000);

        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2");
            }
        }, 2000);
    }
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

与大师约会

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值