【 多线程案例 - 定时器 】


一、什么是定时器

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

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

二、标准库中的定时器

  1. 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
  2. schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).

代码示例:

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

public class TimerScheduleTest {
    public static void main(String[] args) {
        Timer timer = new Timer();
        //new TimerTask是要实现的任务,源码可知这里是实现了Runable接口的
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("10秒到了后实现hello");
            }
        }, 10000);
        System.out.println("先实现main");
    }
}
//实现完以上代码后,该线程并没有结束!是因为Timer内部有专门的线程来负责执行注册的任务,还要等待其他的任务加进来

三、自己实现定时器

3.1 定时器的构成

  1. 一个带优先级的阻塞队列

为啥要带优先级呢?
因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的. 使用带优先级的队列就可以高效的把这个 delay 最小的任务找出来.

  1. 队列中的每个元素是一个 Task 对象.
  2. Task 中带有一个时间属性, 队首元素就是即将
  3. 同时有一个 扫描线程 一直扫描队首元素, 看队首元素是否需要执行

3.2 具体步骤

3.2.1 Task 类用于描述一个任务(作为 Timer 的内部类). 里面包含一个 Runnable 对象和一个 time(毫秒时间戳)这个对象需要放到 优先队列 中. 因此需要实现 Comparable 接口.

// 创建一个类, 表示一个任务.
class MyTask implements Comparable<MyTask> {//该任务是需要将时间短的放在队首,需要比较时间
    // 任务具体要干啥
    private Runnable runnable;
    // 任务具体啥时候干. 保存任务要执行的毫秒级时间戳
    private long time;

    // delay是绝对的时间戳的值,即执行时刻
    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }

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

    public long getTime() {//获取到该任务设置的 执行时刻
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
        //时间小的需要在队列前面
        return (int) (this.time - o.time);
    }
}

注意:

上述任务我们需要通过比较任务的执行时间大小来决定任务优先级,所以要实现 Comparable 接口,手动指定比较规则

3.2.2 Timer 类提供的核心方法为 schedule, 用于注册一个任务, 并指定这个任务多长时间后执行。Timer 实例中, 通过 PriorityBlockingQueue 来组织若干个 Task 对象.通过 schedule 来往队列中插入一个个 Task 对象

class MyTimer {
    // 定时器内部要能够存放多个任务--用带优先级的阻塞队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    public void schedule(Runnable runnable, long delay) {
        MyTask task = new MyTask(runnable, delay);
        queue.put(task);
        // 每次任务插入成功之后, 都唤醒一下扫描线程, 让线程重新检查一下队首的任务看是否时间到要执行~~
        synchronized (locker) {
            locker.notify();
        }
    }

3.3.4 Timer 类中存在一个 扫描线程, 一直不停的扫描队首元素, 看看是否能执行这个任务.

 private Object locker = new Object();
    //在MyTimer类中设置一个扫描线程用来扫描队首任务,看看队首任务是否到达指定时间要去执行
    public MyTimer() {
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    // 先取出队首元素
                    MyTask task = queue.take();
                    // 再比较一下看看当前这个任务时间到了没?
                    long curTime = System.currentTimeMillis();
                    if (curTime < task.getTime()) {
                        // 时间没到, 把任务再塞回到队列中.
                        queue.put(task);
                        // 由于while(true)执行的很快,就会一直获取当前时间和指定时间作比较,忙等比较浪费CPU资源。所以就给扫描线程指定一个等待时间
                        synchronized (locker) {
                            //为啥不用sleep:因为不能被中途唤醒!在等待的途中可能会有新的更前面的任务加入
                            locker.wait(task.getTime() - curTime);
                        }
                    } else {
                        // 时间到了, 执行这个任务
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

注意:

上述代码由于while(true)转的太快了,就会一直获取当前时间和指定时间作比较,这种忙等是比较浪费CPU资源。比如

第一个任务设定的是 1 min 之后执行某个逻辑. 但是这里的 while (true) 会导致每秒钟访问队首元素几万次. 而当前距离任务执行的时间还有很久呢.

所以我们对其进行了优化,引入一个 locker 对象, 借助该对象的 wait / notify 来解决 while (true) 的忙等问题

private Object locker = new Object();

同时做出以下修改:

  1. 修改扫描线程, 引入 wait, 等待一定的时间.扫描线程扫描队首元素后发现还没有达到指定的时间,就会让该线程等待从当前时间到等待时间的时间差,时间到之后就会提醒队首元素执行
  2. 修改 Timer 的 schedule 方法, 每次有新任务到来的时候唤醒一下 扫描线程. (因为新插入的任务可能是需要马上执行的).

注意: 不能引入sleep来进行等待,因为sleep是不能中途唤醒的,而对于扫描线程来说,随时唤醒是非常重要的


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值