【多线程案例】定时器

1. 定时器是什么?

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

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

2. 使用标准库中的定时器

  • 标准库中提供了一个 Timer 类(java.util包下面). Timer 类的核心方法为 schedule .
  • schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后 执行 (单位为毫秒).
public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        }, 3000);
    }

其中TimerTask()就是一个实现了Runnable的抽象类:

可以把它的作用看成是给定时器一个任务,而第二个参数就是指定多久时间后执行这个任务。

3. 手写代码实现定时器

思考一下定时器的构成需要哪些?

  • 一个带有优先级的阻塞式队列
  • 队列中的每一个元素都是一个“任务”对象
  • “任务”对象中包含两个属性,一个属性用于描述任务,也就是一个Runnable,另一个属性用来定义delay。如此一来对手元素就是最即将要执行的任务。
  • 同时需要有一个线程不停的扫描队首元素。看队首元素是否到了执行时间。

1)写一个任务类,任务类还必须能够按照时间来比大小,因为优先级阻塞队列需要比较大小

//任务类 描述任务和任务的delay时间
    static class Task implements Comparable<Task>{
        //任务
        private Runnable command;
        //delay
        private long time;

        public Task(Runnable command,long time){
            this.command = command;
            //时间是在现在的时间的基础上加上delay
            this.time = System.currentTimeMillis() + time;
        }

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

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

 2)需要有一个优先级阻塞队列来存放用户注册的任务

//优先级阻塞队列 核心结构
    //队首存放的是最近要执行的任务 time最小
    private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue();

    public void schedule(Runnable command,long time){
        //生成一个任务 然后放进去优先级队列
        Task task = new Task(command, time);
        queue.put(task);
    }

3)在构造方法中整一个线程对队首元素扫描,看是否到了执行时间

public MyTimer(){
        //在构造方法中来一个扫面线程 一直扫描队首的元素是否到了执行时间
        Thread work = new Thread(() -> {
            while (true) {
                try {
                    Task task = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (task.time > curTime) {
                        // 时间还没到, 就把任务再塞回去
                        queue.put(task);
                    } else {
                        // 时间到了, 可以执行任务
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        work.start();
    }

用写一个测试:

    //测试代码
    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("我有第二个任务!");
            }
        },1000);

        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("我有第三个任务!");
            }
        },2000);
    }

此时已经可以按照定时器的工作原理来完成任务了:

 但是当前的代码还存在着比较严重的问题,就是在3)中如果时间没有到的话会存在cpu一直比较的情况。举个例子,比如小明九点上班,他七点在床上突然醒了。正常情况下应该是继续睡睡到平时订的闹钟时间,但是如果小明一直看表一直看表知道闹铃响起,这样既没有休息也没有做有意义的事情,是十分愚蠢的行为。代码的问题也就在于此,如果没有到执行时间,不管还有多久还都会一直比较有没有到执行时间是没有意义的,也就是处于”忙等“状态。

优化的话,应该让系统在看到当前队首任务还没有到达执行时间的时候就执行wait(时间差)。但是此时还存在另外一个问题,系统wait一段时候之后确实会执行队首的任务,但是如果在wait的时间中又来了新的任务并且新的任务重新处于了队首,此时就会出bug了。正确的做法是在每次有新的任务被注册的时候都通知一下结束wait。

修改代码:

1.引入一个lock对象,借助该对象的wait/notify来解决忙等状态

private Object lock = new Object();

2.修改构造方法中的work的工作方法

public MyTimer(){
        //在构造方法中来一个扫面线程 一直扫描队首的元素是否到了执行时间
        Thread work = new Thread(() -> {
            while (true) {
                try {
                    Task task = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (task.time > curTime) {
                        // 时间还没到, 就把任务再塞回去
                        queue.put(task);
                        // 等待一段时间
                        synchronized (lock){
                            lock.wait(task.time - curTime);
                        }
                    } else {
                        // 时间到了, 可以执行任务
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        work.start();
    }

3. 修改 Timer 的 schedule 方法, 每次有新任务到来的时候唤醒一下 worker 线程. (因为新插入的任务可能是需要马上执行的).

public void schedule(Runnable command,long time){
        //生成一个任务 然后放进去优先级队列
        Task task = new Task(command, time);
        queue.put(task);

        //有新任务来了 唤醒work 检测是否有更新的工作需要执行
        synchronized (lock){
            lock.notify();
        }
    }

完整代码:

public class MyTimer {
    //任务类 描述任务和任务的delay时间
    static class Task implements Comparable<Task>{
        //任务
        private Runnable command;
        //delay
        private long time;

        public Task(Runnable command,long time){
            this.command = command;
            //时间是在现在的时间的基础上加上delay
            this.time = System.currentTimeMillis() + time;
        }

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

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

    //优先级阻塞队列 核心结构
    //队首存放的是最近要执行的任务 time最小
    private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue();

    public void schedule(Runnable command,long time){
        //生成一个任务 然后放进去优先级队列
        Task task = new Task(command, time);
        queue.put(task);

        //有新任务来了 唤醒work 检测是否有更新的工作需要执行
        synchronized (lock){
            lock.notify();
        }
    }

    private Object lock = new Object();

    public MyTimer(){
        //在构造方法中来一个扫面线程 一直扫描队首的元素是否到了执行时间
        Thread work = new Thread(() -> {
            while (true) {
                try {
                    Task task = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (task.time > curTime) {
                        // 时间还没到, 就把任务再塞回去
                        queue.put(task);
                        // 等待一段时间
                        synchronized (lock){
                            lock.wait(task.time - curTime);
                        }
                    } else {
                        // 时间到了, 可以执行任务
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        work.start();
    }

    //测试代码
    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("我有第二个任务!");
            }
        },1000);

        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("我有第三个任务!");
            }
        },2000);
    }
}

此时代码还有问题吗????

理论上说说代码中还是有一点小问题的。(烧脑啊.....)上图:

了解了上述问题之后,就不难发现,问题出现的原因,是因为当前 take 操作,和 wait 操作,并非是原子的如果在 take 和 wait 之间加上锁,保证在这个过程中,不会有新的任务过来,问题自然解决(换句话说只要保证每次 notify 时确实都正在 wait )

猜你喜欢

转载自blog.csdn.net/qq_45875349/article/details/132917641