【Java EE初阶八】多线程案例(计时器模型)

1. java标准库的计时器

1.1 关于计时器

        计时器类似闹钟,有定时的功能,其主要是到时间就会执行某一操作,即可以指定时间,去执行某一逻辑(某一代码)。

1.2 计时器的简单介绍

        在java标准库中,提供了Timer类,Timer类的核心方法是schedule(里面包含两个参数,一个是要执行的任务代码,一个是设置多久之后执行这个任务代码的时间

        注意:Timer内置了线程(前台线程),代码如下所示:

package thread;

import java.util.Timer;
import java.util.TimerTask;

public class ThreadDemo30 {
    public static void main(String[] args) throws InterruptedException {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                // 时间到了之后, 要执行的代码
                System.out.println("hello timer 3000");
            }
        }, 3000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello timer 2000");
            }
        }, 2000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello timer 1000");
            }
        }, 1000);

        System.out.println("hello main");
        Thread.sleep(5000);

        timer.cancel();
    }
}

        结果如下图所示:

【Java EE初阶八】多线程案例(计时器模型)_第1张图片

        代码分析:

        如上图所示,先打印 hello main ,等过了1s才打印 hello 1000,说明Timer内置了线程,main线程不用等待,而timer类是要到时间才会执行任务代码。

        为什么这里可以看到idea里显示线程结束,因为timer类里面有cancel方法,可以结束线程,我们把cancel方法加到打印hello 3000那方法里面,这样就可以结束timer类里面的线程了。

        注意:timer类里面内置的是前台线程,会阻止线程提前结束。

2.  模拟实现一个计时器   

2.1 设计思路

        1、计数器中要存放任务的数据结构
        首先,计时器可以定时去执行一些任务操作,那么我们怎么每次先去执行时靠前的那一操作呢?

        其实在某一些场景下确实可以用数组,但这就需要我们每次都去遍历数组,找出最靠前的时间,但是如果我们要定时很多任务,都需要先找到时间靠前的任务,这就不合理了;从数组里面找出这个时间最靠前的任务数据,一方面要考虑资源花销大的问题,还有要考虑时间的问题,找任务的时间太长,错过了已经到时要执行的任务,如上所述说明使用数组存放任务是不合理的。

        所以就引入了优先级队列,这样每次拿都能拿到时间最小的任务,时间复杂度也仅仅是O(1),但是优先级队列不能是阻塞队列,否则会引起死锁问题。

        2、存放优先级队列中的任务类型:

        我们自定义为任务类MyTimerTask
        任务类是放要执行的代码和要执行任务时间,单独作为一类,存进优先级队列中,其中,优先级队列里的比较规则是按任务类设定的执行时间先后(即时间的大小)来比较的。

        3、计数器类MyTimer
        我们设计一个线程,放在MyTimer类的构造方法中,这个线程就是扫描线程,我们使用该扫描线程来完成判断和操作,主要是入队列或者判断啥时候才执行要执行的代码的操作;同时创建任务schedule的方法里面也包含有入队列的操作。

   2.2 代码实现

        1、MyTimer类:

// 通过这个类, 来表示一个定时器
class MyTimer {
    // 负责扫描任务队列, 执行任务的线程.
    private Thread t = null;
    // 任务队列
    private PriorityQueue queue = new PriorityQueue<>();
    // 搞个锁对象, 此处使用 this 也可以.
    private Object locker = new Object();

    public void schedule(Runnable runnable, long delay) {
        synchronized (locker) {
            MyTimerTask task = new MyTimerTask(runnable, delay);
            queue.offer(task);
            // 添加新的元素之后, 就可以唤醒扫描线程的 wait 了.
            locker.notify();
        }
    }

    public void cancel() {
        // 结束 t 线程即可
        // interrupt
    }

    // 构造方法. 创建扫描线程, 让扫描线程来完成判定和执行.
    public MyTimer() {
        t = new Thread(() -> {
            // 扫描线程就需要循环的反复的扫描队首元素, 然后判定队首元素是不是时间到了.
            // 如果时间没到, 啥都不干
            // 如果时间到了, 就执行这个任务并且把这个任务从队列中删除掉.
            while (true) {
                try {
                    synchronized (locker) {
                        while (queue.isEmpty()) {
                            // 暂时先不处理
                            locker.wait();
                        }
                        MyTimerTask task = queue.peek();
                        // 获取到当前时间
                        long curTime = System.currentTimeMillis();
                        if (curTime >= task.getTime()) {
                            // 当前时间已经达到了任务时间, 就可以执行任务了.
                            queue.poll();
                            task.run();
                        } else {
                            // 当前时间还没到, 暂时先不执行
                            // 不能使用 sleep. 会错过新的任务, 也无法释放锁.
                            // Thread.sleep(task.getTime() - curTime);
                            locker.wait(task.getTime() - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        // 要记得 start !!!!
        t.start();
    }
}

        里面的核心模块:

        1、schedule方法,该方法的创建任务,里面包含了要执行的代码和执行代码的时间,            2、构造方法,里面有一个线程,该线程就是不断去判断队列有没有任务,如果有任务的话,就去找最先执行的任务,等到该任务执行时间就执行扫描到的该任务,如果没到达执行时间的话就要等。

2、MyTimerTask任务类:

// 通过这个类, 来描述一个任务
class MyTimerTask implements Comparable {
    // 在什么时间点来执行这个任务.
    // 此处约定这个 time 是一个 ms 级别的时间戳.
    private long time;
    // 实际任务要执行的代码.
    private Runnable runnable;

    public long getTime() {
        return time;
    }

    // delay 期望是一个 "相对时间"
    public MyTimerTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        // 计算一下真正要执行任务的绝对时间. (使用绝对时间, 方便判定任务是否到达时间的)
        this.time = System.currentTimeMillis() + delay;
    }

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

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

        该任务类里面放的是要执行的任务,和任务执行的延迟时间时间,因为任务要放进优先级队列里,所以要构造一个比较器,用时间参数来进行比较,并且重写compareTo方法,将比较规则具体化。

2.3 计时器的线程安全

        1、维护队列进出的操作---加锁

        不创建其他线程,如果只有一个主线程去调用MyTimer类的话,此时就会有主线程main和 t 线程,这时候,存在线程不安全问题的主线程的代码如下所示:

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

        关于主线程main与t线程存在的线程安全图解如下: 

【Java EE初阶八】多线程案例(计时器模型)_第2张图片

        多线程运行时,会出现同一时刻一个队列存在多个任务进有出的情况,会导致线程不安全;所以,要维护这个队列,就要把入队列和出队列操作都上锁,同一时间要么只能入队列,要么只能出队列;

        对于入队列操作上锁的位置范围,就是把创建任务和入队列操作都上锁;

        对于出队列操作上锁的位置范围,我们要考虑是否把while循环都给上锁了,显然易见,把while上锁的代码十分危险,在我们当前的场景上确实可以用;但是,在其他场景下,如果一个线程拿到锁了,系统就会不停的解锁、加锁,这样会导致其他线程饿死了,所以在while里面加锁,是比较大众的;

        2、优先级队列为空时,设置阻塞等待功能

【Java EE初阶八】多线程案例(计时器模型)_第3张图片

        3、任务没到执行时间,要让该任务等待到固定时间在执行

        代码完善部分如下所示:

【Java EE初阶八】多线程案例(计时器模型)_第4张图片

        代码详解:

        没到任务执行的时间,就要让该任务阻塞等待,且等待时间是: 任务执行的时刻 - 当前的时刻,没有限制要等待的时间的话,就会一直循环,每次循都会环判断是不是到任务执行的时间了,反复循环这个代码执行速度是很快的,但是就会盲等,由此我们不设置任务执行时间的话就会导致计算机资源的浪费;

ps:本次关于计时器的内容就到这里了,如果对大家有所帮助的话,就请一键三连,当然内容可能还会更新,因为未完待续嘛!!!

你可能感兴趣的:(JAVA,EE,初阶,java,java-ee)