Java EE——定时器

定时器

  • 定时器
    • 标准库中的定时器
    • MyTimer
      • MyTask
      • MyTimer
      • 为什么要wait和notify
      • synchronized写法原因
      • 全部代码的实现

定时器

和我们现实中的定时器用途类似,代码中的定时器是设定一个时间,在过去这个时间后,执行某个特定的代码

例如我们的服务器和客户端传递信息,客户端需要等待服务器的信息,如果超出了一定时间还没有收到消息,那么客户端就应该提醒服务器让他重新发一下消息,这里就可以用到计时器

标准库中的定时器

在java.util.Timer包中实现了定时器

首先实现一个Timer对象

Timer timer = new Timer();

然后为计时器布置任务,第一个参数是一个Runnable对象,重写run方法,就可以让定时器到点后执行其中的代码,第二个参数是微秒,

timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("时间到");
            }
        },3000);

我们一个Timer对象,可以安排多个schedule任务

而当我们运行这个代码时,发现程序并没有在执行完代码后退出,这是因为Timer中存在线程来完成任务,之前讲过线程分为前台线程和后台线程(isDaemon判断)而我们的前台线程不会让进程退出

MyTimer

(最后有完整代码,前面的代码只是用来讲解而拆分的)

MyTask

首先我们实现MyTask,代表计时器中的任务对象

class MyTask implements Comparable<MyTask>{
    private Runnable runnable;
    private long time;
    MyTask(Runnable runnable, long delay){
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public long getTime() {
        return time;
    }

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

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

MyTask中有两个参数,一个是Runnable的任务,另一个是时间,时间的大小是现在时刻+传入的时间,我们实现了其构造方法,以及一系列get方法,至于为什么要实现compareTo方法,这个后面会提到

MyTimer

接下来实现MyTimer

class MyTimer{
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    private Object locker = new Object();
    public void schedule(Runnable runnable, long after){

        MyTask myTask = new MyTask(runnable, after);
        queue.offer(myTask);
        synchronized (locker){
            locker.notify();
        }
    }

    public MyTimer(){
        Thread t = new Thread(() -> {
            while(true){
                try {
                    synchronized (locker){
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();
                        if(myTask.getTime() > curTime){
                            queue.put(myTask);
                            locker.wait(myTask.getTime() - curTime);
                        } else {
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        t.start();
    }
}

由于我们需要判断哪个任务先执行,哪个任务后执行,并且如果没有任务了就等待任务,任务满了就不让继续放任务,因此我们创建了一个优先级阻塞队列(详细概念在上一篇博客中有提到),这也是为什么MyTask要实现compareTo的原因

我们在其中添加了schedule方法,在调用这个方法后就可以new一个MyTask任务,并将其放入阻塞队列

在MyTimer的构造方法中,我们创建了一个线程,其主要功能就是循环判断是否有任务需要执行。先从阻塞队列中取出时间最小的任务,然后将其与现在时间进行比较,如果一样了就执行任务,不一样就放回到队列中。

为什么要wait和notify

由于我们不想让线程一直循环执行检测时间的任务,这样太浪费资源了,因此我们思考能不能让线程停一停,首先考虑让线程sleep一个现在时间和任务要执行时间的差值,但是这样有一个问题:如果在这段时间中突然又新增了一个任务,而且这个任务和目前时间的差值比上一个的还小,那么我们就会错过这个任务

因此,我们不能用sleep,那么综合之前几篇博客所讲的,我们可以用wait和motify:在我们线程扫描的时候,wait(当前时间和任务执行时间差值),当我们添加任务的时候,就notify一下睡着了的线程

synchronized写法原因

这时我们还需要考虑synchronized应该圈多大范围的代码块
如果我们的代码块只圈了wait这一条语句,那么可能会出现如下问题

当我们的线程a刚从阻塞队列中take出一个任务,这时另一个线程b就调用了put,插入了一个新的任务,并且这个任务的时间比第一个任务的时间还短,然后线程b一直执行到notify语句,线程a才继续执行,这时线程a还是以第一个任务来计算wait时间的,也就是说我们的线程b的notify并没有对线程a起到作用。

这个问题的出现是因为take和wait计算时间并不是原子性的,从而使线程b有机会插入到其中,因此我们的wait的synchronized代码块应该从take一直圈到wait

那么我们就想到,是不是只要synchronized代码块圈的越大,代码写的就越对呢,事实上并非如此,我们的notify代码如果和queue.offer()方法被圈到了一起,就会出现死锁问题,这是因为我们的queue是一个阻塞队列,其put的实现也是带有synchronized,但是我们put的synchrozed传入的对象和notify使用的对象是不一样的,这样的话代码就会一直阻塞在put

因此我们可以发现,线程是非常麻烦而且容易出错的,这也是为什么其他编程语言尝试简化多线程
例如erlang的actor模型,go的CSP,python的await/async
但是在java和cpp中,多线程是最基本的编程方式

全部代码的实现

import java.util.concurrent.PriorityBlockingQueue;

class MyTimer{
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    private Object locker = new Object();
    public void schedule(Runnable runnable, long after){

        MyTask myTask = new MyTask(runnable, after);
        queue.offer(myTask);
        synchronized (locker){
            locker.notify();
        }
    }

    public MyTimer(){
        Thread t = new Thread(() -> {
            while(true){
                try {
                    synchronized (locker){
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();
                        if(myTask.getTime() > curTime){
                            queue.put(myTask);
                            locker.wait(myTask.getTime() - curTime);
                        } else {
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        t.start();
    }


}

class MyTask implements Comparable<MyTask>{
    private Runnable runnable;
    private long time;
    MyTask(Runnable runnable, long delay){
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public long getTime() {
        return time;
    }

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

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

public class demo {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("时间到");
            }
        },3000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("时间到2");
            }
        },4000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("时间到3a");
            }
        },5000);
    }
}

你可能感兴趣的