java中的定时器

1. 标准库中的定时器

所谓的定时器,其实就是闹钟。我们可以设定一个时间,当这个时间到了,就可以执行一个指定的代码。java 标准库中提供的定时器:java.util.Timer。代码演示:

public class ThreadTest6 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        }, 2000);
    }
}

Timer 的核心方法是 schedule,该方法有两个参数,第一个参数就是我们准备执行的任务,第二个参数就是时间,即过多久执行任务。
这里我们使用了 new TimerTask 来表示一个任务,这个 TimerTask 本质上是一个 Runnable,我们可以看看其源码:
在这里插入图片描述
由此可见 TimerTask 是一个实现了 Runnable 接口的抽象类,所以我们还是要重写 run 方法,这个 run 方法的内容也就是我们要执行的任务。
我们执行代码:
在这里插入图片描述
我们可以很直观的看到,隔了 2s 打印了 hello。
此时细心的朋友会发现,我们这个程序并没有退出,还在继续执行。原因是 Timer 里头内置了前台线程,前台线程会阻止进程结束。其实 run 方法的执行是依靠 Timer 内部的线程在时间到了之后执行的!

2. 自己实现一个定时器

我们要向自己实现一个定时器,肯定要参考 java 标准库的效果,然后我们发现,定时器,内部管理的不仅仅是一个任务,可以管理很多任务的,所以我们要把这一点考虑进去。
实现这个管理多个任务,最容易想到的方法就是创建多个线程,每个任务对应一个线程,但是我们在进一步想想就觉得这个方法不好,因为万一有1w个任务,我们创建1w个线程不合适。此时继续思考,聪明的我们就能意识到,虽然任务可能很多,但是他们的出发时间是不同的没所以说我们其实只需要 一个/一组 工作线程,每次都找到这些任务中离执行时间最近的任务就可以,也就是说一个线程先执行最早的任务,做完了再执行第二早的文物… 时间到了就执行,没到就再等等。所以说我们就需要知道最小的时间,这个时候我们又是很容易的想到排序,排序确实可以解决问题,但是排序效率比较低,而且如果出现插入操作也很麻烦,那么更高效方便的方法就是用堆,即PriorityQueue。除此之外,我们还希望在多线程操作下,优先级队列还能线程安全,而标准库中提供了带优先级的阻塞队列,PriorityBlockingQueue。
下面进行代码实现:

// 表示一个任务
class MyTask{
    public Runnable runnable;
    // 为了后续判定方便,我们使用绝对的时间戳
    public long time;

    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        // 取当前时间戳 + time,作为任务实际执行的时间戳
        this.time = System.currentTimeMillis() + time;
    }
}

class MyTimer{
    PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    public void schedule(Runnable runnable, long time) {
        MyTask task = new MyTask(runnable,time);
        queue.put(task);
    }

    // 创建线程,负责执行任务
    public MyTimer(){
    Thread t = new Thread(() -> {
        while (true) {
            try {
                MyTask task = queue.take();
                long curTime = System.currentTimeMillis();
                if (task.time <= curTime) {
                    // 时间到了,执行任务
                    task.runnable.run();
                }else {
                    // 时间没到,将任务放回队列
                    queue.put(task);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t.start();
    } 
}

代码写到这里其实逻辑已经很清晰明了了,但是还存在两个非常严重的 bug,也算是细节方面还没有完全处理好:

  1. 优先级队列缺少比较规则,我们说了根据时间,但是显然我们并没有实现。
  2. 忙等:确实是在等,但是 CPU 没捞到休息(等待过程占用着 cpu)。举个例子,假设现在是12:00,在12:30的时候要做一件事,我们在12:00是看一下表,时间没到,然后立即有看一下表,又没到,然后…,所以我们一直在看表,没有休息,。这是不科学的,所以我们就需要在等待过程中释放 CPU。

下面我们就来完善一下代码:

// 表示一个任务
class MyTask implements Comparable<MyTask> {
    public Runnable runnable;
    // 为了后续判定方便,我们使用绝对的时间戳
    public long time;

    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        // 取当前时间戳 + time,作为任务实际执行的时间戳
        this.time = System.currentTimeMillis() + time;
    }

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

class MyTimer{
    PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    private Object locker = new Object();

    public void schedule(Runnable runnable, long time) {
        MyTask task = new MyTask(runnable,time);
        queue.put(task);
        synchronized (locker) {
            locker.notify();
        }
    }

    // 创建线程,负责执行任务
    public MyTimer(){
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    synchronized (locker) {
                        MyTask task = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (task.time <= curTime) {
                            // 时间到了,执行任务
                            task.runnable.run();
                        }else {
                            // 时间没到,将任务放回队列
                            queue.put(task);
                            wait();
                            locker.wait(task.time - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t.start();
    }
    
}      MyTask task = new MyTask(runnable,time);
        queue.put(task);
        synchronized (locker) {
            locker.notify();
        }
    }

    // 创建线程,负责执行任务
    Thread t = new Thread(() -> {
        while (true) {
            try {
                synchronized (locker) {
                    MyTask task = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (task.time <= curTime) {
                        // 时间到了,执行任务
                        task.runnable.run();
                    }else {
                        // 时间没到,将任务放回队列
                        queue.put(task);
                        locker.wait(task.time - curTime);
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

比较器很简单,不用多说。而为什么我们使用 wait 等待而不用 sleep呢,因为 wait 可以随时唤醒,假设在等待过程中插入了一个新的任务,这个新任务的时间更快,那么 notify 就可以唤醒 wait,下次在从队列里拿任务就会拿到更快的这个任务而如果使用 sleep,就会睡眠 设定好的时间,如果在这期间插入了新的、更快的任务,就会导致这个任务执行晚了,出现 bug。
我们在 main 方法中使用一下自己写的定时器,看看效果:

public class ThreadTest7 {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello1");
            }
        }, 1000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello3");
            }
        }, 3000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello4");
            }
        }, 4000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello2");
            }
        }, 2000);
    }
}

运行结果:
在这里插入图片描述
very good。
但是还没有结束,我们知道使用 wait 要搭配 synchronized,那么为什么我们 synchronized 要放在我们代码中的这个位置:

Thread t = new Thread(() -> {
            while (true) {
                try {
                    synchronized (locker) {
                        MyTask task = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (task.time <= curTime) {
                            // 时间到了,执行任务
                            task.runnable.run();
                        }else {
                            // 时间没到,将任务放回队列
                            queue.put(task);
                            locker.wait(task.time - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

如果加在这里会怎么样:

Thread t = new Thread(() -> {
            while (true) {
                try {
                    MyTask task = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (task.time <= curTime) {
                        // 时间到了,执行任务
                        task.runnable.run();
                    } else {
                        // 时间没到,将任务放回队列
                        queue.put(task);
                        synchronized (locker) {
                            locker.wait(task.time - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

加在这里可能产生 bug,假设当前时间为 12:00,现在最近的一个任务1是 12:30,那么就有可能出现这样一种情况:
在这里插入图片描述
此时当线程1执行完 put 操作,还没进行 wait 时,由于线程调度的随机性,很有可能就把这个线程1调度走了,然后此时又有另一个任务2插进来,这个任务2的执行时间为 12:15,此时由于插入任务2而引起的 notify 操作相当于空打一炮,没有作用,因为 线程1 还没有 wait:
![在这里插入图片描述](https://img-blog.csdnimg.cn/4af036d4c7564fb3800bb144c337b53a.png
当线程1调度回来继续执行时,此时 进入 wait 的时间是 12:30 - 12:00 = 30分钟,而不是 15 分钟,就会导致任务2错过正确执行时间。但是我们一开始的 synchronized 的位置就可以保证代码从 take 到 wait 这一段都是原子的:
在这里插入图片描述
此时即使出现上述情况,线程2也不能执行 notify,因为锁在 线程1这里没有被释放,所以线程2会阻塞等待锁,只有线程1释放了锁,线程2才能执行notify,但是此时线程1已经处于 wait 状态了,即 notify 不会出现空打一炮的情况。

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不想菜的鸟

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

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

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

打赏作者

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

抵扣说明:

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

余额充值