[线程] 定时器 及 实现定时器

一. 定时器

定时器, 就是"闹钟"的效果
指定一个任务(Runnable), 并且指定一个时间, 此时这个任务不会立即执行, 而是在时间到达后, 再去执行
在日常开发中, 定时器是一个非常重要的基础组件, 甚至会把定时器功能单独封装成服务器, 供整个分布式系统来使用

二. 标准库中的定时器

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

 public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 3000");
            }
        },3000);
        //可以设多个定时器
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 2000");
            }
        },2000);

    }

Timer内部包含了线程, 把线程创建封装起来了, 并且是前台线程, 阻止进程的结束
在这里插入图片描述

三. 实现定时器

1. 创建MyTimerTask类

需要包括执行的任务, 和执行任务的绝对时间(时间戳)

为什么用绝对时间,而不是用delay
为了方便后续执行任务的时候, 可以方便判定, 该任务是否能够执行
如果是delay, 随着时间的推移, 还需要更新delay, 比较麻烦

class MyTimerTask{
    private Runnable runnable;
    private long time;
    public MyTimerTask(Runnable runnable, long delay){
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }
    public void run(){
        runnable.run();
    }
    public long getTime(){
        return this.time;
    }
}

2. 通过一定的数据结构, 保存多个任务

如果我们使用ArrayList
因为我们要保证, delay时间短的先执行, 那么我们就需要不停地循环扫描ArrayList这里的每个任务, 判定是否要执行
更好的办法就是使用优先级队列
把这些任务通过优先级队列保存起来, 按照时间作为优先级的先后标准, 就可以做到, 队首元素就是时间最靠前的任务, 那么执行任务的时候, 只需要取队首元素即可
在这里插入图片描述
那么, 使用优先级队列, 就需要里面的对象实现Comparable接口, 那么MyTimerTask就需要修改一下:

class MyTimerTask implements Comparable<MyTimerTask>{
    private Runnable runnable;
    private long time;
    public MyTimerTask(Runnable runnable, long delay){
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }
    public void run(){
        runnable.run();
    }
    public long getTime(){
        return this.time;
    }

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

因为我们要实现的是小根堆, 在compareTo方法中, 是 谁 - 谁, 我们不要去背, 随便写一个试试就知道对不对了, 背很容易背错!!!

3.MyTimer类的构造方法

在MyTimer类的构造方法中, 创建一个线程, 用这个线程来执行队列中的任务

public MyTimer(MyTimerTask myTimerTask, long delay){
        Thread thread = new Thread(() -> {
            while(true){
                //如果任务队列为空,就重新判断是否要执行
                if(queue.size() == 0){
                    continue;
                }
                MyTimerTask task = queue.peek();
                //如果此时的时间 >= 要执行的任务的时间, 则执行任务, 并出队列, 反之则重新判断
                if(task.getTime() >= System.currentTimeMillis()){
                    task.run();
                    queue.poll();
                }else{
                    continue;
                }
            }
        });
        thread.start();
    }

改进:

  1. 上述代码中, 如果任务队列为空或者还没到执行的时间, 就会处于忙等的状态, 短时间内循环很多次, 直到队列不为空或等到执行时间, 会一直消耗cpu资源, 但是并没有执行真正的任务, 所以我们要让线程处于等待的时候, 能够释放cpu
    所以可以使用 wait
  2. PriorityQueue本身是线程不安全的, 所以要加上锁

为什么不用PriorityBlockingQueue?
因为PriorityBlockingQueue只能处理队列为空的时候阻塞, 不能处理时间没到的阻塞
如果使用PriorityBlockingQueue, 还是需要再加一把锁, 此时代码就更加复杂了引入了两把锁, 稍有不慎, 就容易引起死锁!

public MyTimer(MyTimerTask myTimerTask, long delay){
        Object locker = new Object();//创建锁对象
        Thread thread = new Thread(() -> {
            try{
                while(true){
                    synchronized (locker){
                        //如果任务队列为空,就阻塞等待
                        if(queue.size() == 0){
                            locker.wait();
                        }
                        MyTimerTask task = queue.peek();
                        //如果此时的时间 >= 要执行的任务的时间, 则执行任务, 并出队列, 
                        // 反之则阻塞等待task.getTime() - curTime这些时间, 时间到了就执行任务
                        long curTime = System.currentTimeMillis();
                        if(task.getTime() >= curTime){
                            task.run();
                            queue.poll();
                        }else{
                            locker.wait(task.getTime() - curTime);
                        }
                    }
                }
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        });
        thread.start();
    }

那么上述等待为什么不用sleep?

  1. sleep睡着了就是真的睡着了, 不能像wait一样可以唤醒
    此时, 如果来了一个比队首元素执行时间还要进的元素, 那么我们就需要唤醒等待, 并且重新安排等待时间, 这时sleep做不到的
  2. 最重要的一点, 就是sleep等待期间不会释放锁, wait会释放锁
    如果不释放锁, 怎么进行入队操作呢?

4. 实现schedule方法

这个方法主要是为了添加任务, 并创建线程, 利用线程完成任务

public void schedule(Runnable runnable, long delay){
       synchronized (locker){
           MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
           queue.offer(myTimerTask);
           //每次添加新的任务, 都要唤醒wait, 重新判断队首元素是否要执行
           locker.notify();
       }
    }

完整代码:

class MyTimerTask implements Comparable<MyTimerTask>{
    private Runnable runnable;
    private long time;

    public MyTimerTask(Runnable runnable, long delay){
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }

    public void run(){
        runnable.run();
    }

    public long getTime(){
        return this.time;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        return (int)(this.time - o.time);
        //return (int)(o.time - this.time);
    }
}
class MyTimer{
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    private Object locker = new Object();//创建锁对象


    public MyTimer(){
        Thread thread = new Thread(() -> {
            try{
                while(true){
                    synchronized (locker){
                        //如果任务队列为空,就阻塞等待
                        if(queue.size() == 0){
                            locker.wait();
                        }
                        MyTimerTask task = queue.peek();
                        //如果此时的时间 >= 要执行的任务的时间, 则执行任务, 并出队列,
                        // 反之则阻塞等待task.getTime() - curTime这些时间, 时间到了就执行任务
                        long curTime = System.currentTimeMillis();
                        if(task.getTime() <= curTime){
                            task.run();
                            queue.poll();
                        }else{
                            locker.wait(task.getTime() - curTime);
                        }
                    }
                }
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        });
        thread.start();
    }

    public void schedule(Runnable runnable, long delay){
       synchronized (locker){
           MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
           queue.offer(myTimerTask);
           locker.notify();
       }
    }
}
public class Demo30 {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 3000");
            }
        },3000);
        //可以设多个定时器
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 2000");
            }
        },2000);
    }
}

执行结果:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值