【多线程】详解——模拟设计Timer
  TEZNKK3IfmPf 2024年03月30日 55 0

前言

        在服务器开放中,客户端向服务器发送请求,等待服务器响应,但若因为某个故障,导致程序一直无响应,怎么办?这时总不可能让客户端一直都没有响应,一直等下去,很有可能程序就卡死了,所以为了应对此种情况,程序员就设置了一个定时器,若到在定时器规定的时间没有完成任务,会执行某一个动作,响应客户端;


Timer标准库中用法

        标准库中也有定时器(如下图)

【多线程】详解——模拟设计Timer(结尾附码源)

         标准库中定时器这个类里有个schedule方法,就是用来安排在多长时间之后来执行安排好的任务,因此他有两个参数(如下图)

【多线程】详解——模拟设计Timer(结尾附码源)

         一个参数是要执行的任务,就是一个Runnable,需要继承TimerTask,重写run方法,从而指定要执行的任务; 另一个参数是等待时间(单位是毫秒),也即是经过多长时间后,执行任务;

        一个定时器中,可以同时安排多个任务,并且执行完任务之后,进程并没有退出,Timer内部需要一组线程来执行任务,这里的线程才是“前台线程”,会影响进程的退出;

如下代码:(安排一个时间表)

    public static void main(String[] args){
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("12:00 唱歌~");
            }
        }, 2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("12:30 弹钢琴");
            }
        }, 3000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("13:00 弹吉他");
            }
        }, 4000);
        System.out.println("开始玩音乐");
    }

执行结果如下:

【多线程】详解——模拟设计Timer(结尾附码源)


Timer模拟实现

一、描述一个任务

        schedule方法的第一个参数便是任务,所以我们需要能够描述一个任务,怎么描述呢?这里的任务需要包含两个信息,一个是要干什么事(Runnable),另一个是要什么时候执行;(如下图)

【多线程】详解——模拟设计Timer(结尾附码源)

分析:

        知道了任务和要执行的时间,这样就够了吗?想象一下,刚刚提到,一个定时器可以同时安排多个任务,那么这多个任务必然会涉及到先后发生的问题,那么供大于求的时候也就需要涉及到排序,可是任务是个类,怎么排序呢?比较器——继承Comparable接口,重写compareTo方法;(如下图) 

【多线程】详解——模拟设计Timer(结尾附码源)

分析:

        这里由于time是long类型,所以需要强制类型转换,还有一个问题:“这里的return到底是this.time - o.time 还是 o.time - this.time 呢?”这里最好不要背,比较比较抽象,返回规则不是那么直观,所以,你只需要随便写一个,运行程序试一下,这多试几次,不也就知道吗~

二、创建MyTimer类,让MyTimer管理多个任务

        可以用ArrayList吗?刚刚咱提到当供大于求时任务存储需要按序存储,ArrayList显然时无序,不方便找到时间最小的任务,所以这里不难想到使用优先级队列,但由于schedule时有可能在多线程中调用的,并且优先级队列并不是线程安全的,所以可以使用优先级阻塞队列(BlockingQueue)来实现;

四、创建一个扫描线程

        从队列中取元素,可以用一个“扫描线程”来不停的检查队首元素,检查时间是否到了,若时间到了,则取出队首元素,执行任务,若时间没到,则将队首元素放回;(如下图)

【多线程】详解——模拟设计Timer(结尾附码源)

 分析:

        由于是阻塞队列,所以因该先取出任务,才能够判断时间是否已经到了,若时间还没到,就把任务继续放回队列

五、各类问题及解决办法

存在问题分析一:“忙等”

        观察代码,不难发现当时间还没到的时候,队列会循环的将队首元素取出,然后放回,这个循环就是在忙等,CPU并没有空闲出来,因此这里的等待也就变的毫无意义;

        注意这里不可以用sleep(),假设当前时间为9点,任务时间是10点,那么你将设置 sleep 的时间为一个小时,这样合理吗?看似合理,但在多线程的情况下,很有可能会有新的任务出现,若新的任务时间为9点半,那么新的任务就需要执行sleep到10点,那么就会导致新的任务无法在9点半的时候执行!

解决办法:

        使用wait()就可以很好的解决上述问题;如下图

【多线程】详解——模拟设计Timer(结尾附码源)

【多线程】详解——模拟设计Timer(结尾附码源)

存在问题分析二:“notify()空唤醒”

       假设当前时间为9点,而当前任务执行的时间为11点,试想如果schedule在t1线程刚执行完take,还没有到wait的时候又新增了一个任务,这个任务的执行时间为10点,这会发生什么?

        首先notify会在t1线程还没有wait的时候先唤醒一次,就相当于啥也没做,但其实notify即使什么线程都没唤醒,并不会存在什么问题,但是严重的问题在后面!当扫描线程继续往下执行,发现时间还没到,就将刚刚take出来的任务又放回了阻塞队列,然后就进行了wait等待,这以等待便是2个小时,这意味着,刚才新来的任务需要在10点的时候执行,却被错过了!

解决办法:

        刚才出现问题的原因实际上就是因为notify在take和wait之间执行的,现在只需要把扫描线程中的锁范围扩大,保证take和wait之间这段操作的原子性,这样,就可以避免notify在take和wait之间执行;

        来分析一下具体过程,9点的时候来了一个任务,需要在11点执行,扫描线程会先拿到锁,然后take,(这时将一个新的任务放入了队列,这个任务的执行时间为10点),接着实现中间逻辑,发现没到时间,就将take出的元素放入队列(此时他已经不是队首元素了),最后到wait;在这个过程中,schedule线程会一直阻塞等待锁,当扫描线程执行了wait,释放了锁,这个时候需要在10点开始执行的任务才拿到锁,执行notify唤醒了正在wait的线程,于是,继续循环取出队首元素,而此时,取出的队首元素便是要在10点执行的那个任务;(如下图)

【多线程】详解——模拟设计Timer(结尾附码源)

这样写,就已经基本没问题了;

最后可以思考这样一个问题:若这样写(如下图),会存在什么问题?

【多线程】详解——模拟设计Timer(结尾附码源)

 问题分析:“死锁”

        假设当我new了一个刚刚写好的MyTimer,那么此时就会初始化并调用MyTimer构造方法,构造方法中会创建一个线程t1,并开始线程,那么就会执行run方法,此时线程t1拿到锁,程序进入run方法,接着进行take操作,但是阻塞队列中没有元素,因此阻塞队列就会阻塞等待,直到有任务放入队列中,此时,来了一个任务,通过myTimer.schedule(//任务...,//时间...),此时进入schedule方法,但是由于刚刚线程t1已经拿到锁,schedule也会阻塞等待,所以此时schedule无法将任务put到队列中,这时t1在阻塞等待,schedule也在阻塞等待,就出现了死锁;

        所以一定要把put操作放在所外面!


模拟定时器代码

//任务
class MyTask implements Comparable<MyTask>{
    //将要执行的任务
    private Runnable runnable;
    //多久后执行任务
    private long time;
    public MyTask(Runnable runnable, long time){
        this.runnable = runnable;
        this.time = time + System.currentTimeMillis();
    }
    public Runnable getRunnable() {
        return runnable;
    }
    public long getTime() {
        return time;
    }
    public int compareTo(MyTask o) {
        return (int)(this.time - o.time);
    }
}
//计时器
class MyTimer {
    //优先级阻塞队列来存放任务,保证时间最小的先出队
    private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    private Object locker = new Object();
    public MyTimer() {
        //创建一个线程,不断来扫描下一个任务
            Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    synchronized(locker) {
                        try {
                            //取出队首元素
                            MyTask task = queue.take();
                            //获取当前时间
                            long nowTime = System.currentTimeMillis();
                            //若到时了就执行任务,若没到时就阻塞等待
                            if(nowTime >= task.getTime()){
                                task.getRunnable().run();
                            }else{
                                //先将取出的元素放回
                                queue.put(task);
                                locker.wait(task.getTime() - nowTime);
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
            t1.start();
    }
    //安排
    public void schedule(Runnable runnable, long time) throws InterruptedException {
        queue.put(new MyTask(runnable, time));
        synchronized(locker) {
            locker.notify();
        }
    }
}
//测试
public class Test {
    public static void main(String[] args) throws InterruptedException {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("执行任务1");
            }
        }, 1000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("执行任务2");
            }
        },2000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("执行任务3");
            }
        },3000);
        System.out.println("开始执行任务");
    }
}
【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2024年03月30日 0

暂无评论

推荐阅读
TEZNKK3IfmPf