【JavaEE】定时器实例

目录

▮定时器实例 

▮定时器的内部实现原理

▪阻塞优先级队列

▪静态内部类Task

▪方法 schedule(任务,倒计时)

▪一个进程,执行定时器

▪构造方法

▪完整代码

▮定时器的一些细节

▪任务的传递

▪多线程理解

▪锁所起到的作用



▮定时器实例 

        定时器是一个非常实用的一个工具,你能给定它一个任务和一个倒计时,等倒计时一到,它就会自动去执行这个任务。

       首先,定时器是java.util包中一个的一个类:Timer,要先有一个对象才能使用。

public class demo01 {
    public static void main(String[] args) {
        Timer timer = new Timer();
    }
}

        然后,使用方法:schedual()来添加任务和倒计时。这个方法有两个参数(任务,倒计时)。任务的类型是TimerTask抽象类,而倒计时是一个long。 TimerTask和Runnable接口很像,可以重写run()来写入任务代码;倒计时是以毫秒为单位,1000就等于1s。

public class demo01 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        //tiem.schedule(任务,倒计时);
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("一秒后打印:任务1");
            }
        },1000);
    }
}

        这样一来,一个简单的定时器实例就完成了。程序执行1s后,会自动打印一个“一秒后打印:任务1”。

•TimerTask

public abstract class TimerTask implements Runnable {
    //其它成员
}

        TimerTask是一个实现了Runnable接口的抽象类,所以可以把它当作Runnabl接口,通过重写里面的run()方法来实现添加任务

        还有一点需要强调,定时器执行完任务后并不会结束程序,而是在那等待。因为一个定时器可能不止安排一个任务,所以一个定时器执行玩所有任务后会进入待机状态。

public class demo01 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("一秒后打印:任务1");
            }
        },1000);
        //同一个定时器添加了第二个任务
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("二秒后打印:任务2");
            }
        },2000);
    }
}

                 任务结束后,程序并没有出现下方结束的标志。所以说定时器结束任务后进程不会结束

▮定时器的内部实现原理

class Timer{

        //1.一个阻塞优先级队列

        //2.一个静态内部类Task

        //3.方法schedule(任务,时间)

        //4.一个进程,执行定时器

        //5.一个锁

        //6.Timer的构造方法。

}

        上面就是一个定时器的6个核心部位,接下来我们来一一讲解。

▪阻塞优先级队列

        因为一个定时器里面可以安排多个任务,所以使用“队列”数据结构来装入任务。又因为要按照倒计时的大小对任务进行排序,所以使用优先级队列也就是堆结构来排序。最后,使用阻塞队列来让定时器能进入待机状态,不会因为任务结束而停止定时器。

        Java中给我们提供了阻塞优先级队列,所以我们可以直接使用,至于它内部的实现原理,我们暂时不谈,知道阻塞队列是什么,就差不多知道这是个什么了。

class MyTimer{
    //PriortyBlockingQueue<T>:阻塞优先级队列
    private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
}

▪静态内部类Task

        上面我们看到,阻塞优先级队列的泛型是Task。所以说,这个内部类就是要往队列里添加的元素。

        一个任务有两个成员,一个是他的行为,一个是他的倒计时。行为用Thread来接收,倒计时用long

private static class Task {
        Thread task;//任务
        long time;//倒计时
}

        再给个构造方法,时间我们取现在的时间+给出的倒计时。构造方法在定时器被安排任务的那一刻启动,这样就求得了任务执行的具体时间,而不再是一个倒计时。

private static class Task{
        Thread task;
        long time;
        //构造方法
        public Task(Runnable runnable,long time){
            task = new Thread(runnable);
            this.time = System.currentTimeMillis() + time;
        }       
    }
}

        最后,不要忘记了我们的Task是要添加进优先级队列的,我们引入比较接口来为task提供比较的方法,让队列能把时间短的任务给排到队头。

private static class Task implements Comparable<Task>{//泛型Task,因为要跟其它Task比较

        Thread task;//任务
        long time;//具体执行时间,不再是倒计时

        //构造方法
        public Task(Runnable runnable,long time){
            task = new Thread(runnable);
            this.time = System.currentTimeMillis() + time;
        }

        //比较方法
        @Override
        public int compareTo(Task o) {
            return (int)(time - o.time);//记得转成int类型
        }
        
        //提供一个run()方法来执行里面的任务
        public void run(){
            task.run();
        }
    }
}

▪方法 schedule(任务,倒计时)

        这个方法是用于定时器安排任务的,所以要创建一个Task对象来接收这个任务,然后将任务添加队列。

    public void schedule(Runnable command,long after){
        //创建Task对象
        Task task = new Task(command,after);
        //将对象加入阻塞优先级队列
        queue.offer(task);

        /* 这段下文会解释,目前不管
         synchronized (locking){
            locking.notify();
        }
        */
    }

▪一个进程,执行定时器

        新建一个线程来执行定时器。定时器执行的逻辑是:取得目前时间和列头任务;如果当前时间大于等于任务时间,则执行此任务;反之则将此任务再次添入队列。最后,while重复执行上述逻辑。

private Thread t = new Thread(() -> {
        while (true) {
                //当前时间
                long curTime = System.currentTimeMillis();
                //队头任务
                Task task = queue.take();

                if(curTime >= task.time){
                    task.run();
                }else{
                    queue.offer(task);
                }
        }
    });

        这里有个盲等的问题:如果,当前任务时间未到;那么,这个任务会被丢到队列;接着,优先级队列又把它给排到队头。一直这样重复到任务时间到。这种盲等的问题是毫无意义的,还会占用CPU资源。所以我们创建一个锁对象,然后使用wait()和notify()来解决这个问题。给wait传入(任务时间-现在时间),等待任务执行前的空白时间。

    
    //创建了一个锁对象
    Object locking = new Object();

    private Thread t = new Thread(() -> {
        while (true) {
            synchronized (locking) {//上锁
                try {//wait()方法所需的返回异常

                    long curTime = System.currentTimeMillis();
                    Task task = queue.take();

                    if(task.time <= curTime){
                        task.run();
                    }else{
                        queue.offer(task);
                        //等待任务前的空白时间
                        locking.wait(task.time - curTime);

                    }

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    });

        因为wait有限定时间,所以可以不用notify()唤醒的。但是呢有一种情况,就是在等待的过程中,定时器又被重新安排一个任务,这个任务是一个更早执行的任务。如果此时定时器还在等待上一个任务,那就会错过这个新任务的执行时间。所以我们在schedual里添加一个notify(),在每次添加新任务后,都重新对队头任务进行判断。

    public void schedule(Runnable command,long after){
        Task task = new Task(command,after);
        queue.offer(task);
        //唤醒wait
        synchronized (locking){
            locking.notify();
        }
    }

▪构造方法

        定时器的构造方法很简单,就是在定时器创建时,启动定时器线程。

public MyTimer(){
        t.start();
    }

▪完整代码

import java.util.concurrent.PriorityBlockingQueue;

class MyTimer{
    //构造方法
    public MyTimer(){
        t.start();
    }
    //PriortyBlockingQueue<T>:阻塞优先级队列
    private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
    //方法:安排任务
    public void schedule(Runnable command,long after){
        //创建Task对象
        Task task = new Task(command,after);
        //将对象加入阻塞优先级队列
        queue.offer(task);

        // 这段下文会解释,目前不管
         synchronized (locking){
            locking.notify();
        }
    }

    //创建了一个锁对象
    Object locking = new Object();

    //定时器进程
    private Thread t = new Thread(() -> {
        while (true) {
            synchronized (locking) {//上锁
                try {//wait()方法所需的返回异常

                    long curTime = System.currentTimeMillis();
                    Task task = queue.take();

                    if(task.time <= curTime){
                        task.run();
                    }else{
                        queue.offer(task);
                        //等待任务前的空白时间
                        locking.wait(task.time - curTime);

                    }

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    });
    //静态内部类 Task
    private static class Task implements Comparable<Task>{//泛型Task,因为要跟其它Task比较

        Thread task;//任务
        long time;//具体执行时间,不再是倒计时

        //构造方法
        public Task(Runnable runnable,long time){
            task = new Thread(runnable);
            this.time = System.currentTimeMillis() + time;
        }

        //比较方法
        @Override
        public int compareTo(Task o) {
            return (int)(time - o.time);//记得转成int类型
        }

        //提供一个run()方法来执行里面的任务
        public void run(){
            task.run();
        }
    }
}

▮定时器的一些细节

▪任务的传递

       首先,在给定时器添加任务的时候,我们使用了TimerTask这个类来定义任务。因为TImerTask实现了Runnable接口,所以我们本质上还是在使用Runnable;然后,在schedule()中,我们用了一个Runnable的形参来接收任务对象;最后,我们把这个TimerTask对象赋给了一个类型Thread的引用。

        一个TimerTask对象,重写了Runnable接口的run(),最后传给了一个Thread的引用。

new TimerTask() {
    @Override
    public void run() {
        System.out.println(1);
    }
}

public void schedule(Runnable command,long after){
    Task task = new Task(command,after);
}

public Task(Runnable command,long time){
    command = new Thread(runnable);
    this.time = System.currentTimeMillis() + time;
}

▪多线程理解

        定时器里只有一个线程,定时器在这个线程里执行的是任务的run()方法,而不是给任务开创新的线程去执行。

        schedule()方法是其它线程在调用,不在定时器线程中使用。任务的执行由定时器来负责,而任务的安排由使用定时器的线程来安排。

▪锁所起到的作用

        这里面的锁起到了两个作用,一个是保护了代码的原子性,一个是wait()来等待。这里的锁并没有起到防止竞争的作用,因为根本就没有其它线程来跟定时器竞争任务。

        wait()等待作用已经在上面讲过了,那我们就细谈一些忙等问题。下方代码中,锁的位置有所不同。案例1是我们正确的写法,而案例2没能保护代码原子性。为什么了?

        假如定时器线程在取得当前时间curTime和任务task后,其它线程给定时器安排了一个时间比当前任务更早的任务。前面我们知道,schedaul()方法里有个notify(),这个notify()是用来唤醒当前任务的等待。但是吧,此时的定时器还没走到wait()这一步来,所以这个notify就唤醒了一个寂寞。可是,定时器中的wait()依旧执行。这样一来,我们新任务就要等待旧任务的空白时间。

        这显然是存在问题的。问题就在于,定时器线程中,获得当前任务和时间代码和执行wait()代码不是原子的,所以我们使用了案例1这种写法。

/*
** 案例1
*/
 private Thread t = new Thread(() -> {
        while (true) {
            synchronized (locking) {//上锁
                try {
                    long curTime = System.currentTimeMillis();
                    Task task = queue.take();

                    if(task.time <= curTime){
                        task.run();
                    }else{
                        queue.offer(task);
                        locking.wait(task.time - curTime);
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    });

/*
** 案例2
*/
 private Thread t = new Thread(() -> {
        while (true) {
            long curTime = System.currentTimeMillis();
            Task task = queue.take();
            if(task.time <= curTime){
                task.run();
            }else{
                queue.offer(task);
                synchronized (locking){//上锁
                    try {
                        locking.wait(task.time - curTime);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    });

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值