目录

 8.4定时器

定时器是什么

标准库中的定时器

答疑:

Q1:程序好像也没有正常结束

Q2:抽象类不是不能创建对象吗??

实现定时器

1.创建一个类,表示一个任务

2.定时器中,能够管理多个任务的,必须使用一些集合类把这多个任务给管理起来

思考:那什么集合类来管理呢?

3.实现 schedule 方法,把任务添加到队列中即可

4.额外创建一个线程,负责执行队列中的任务

①先创建个锁对象:

②给 schedule 加锁:

③给构造对象 MyTimer 里的线程加锁

④接下来我们在 main 方法里写个方法验证一下定时器效果

基于堆(优先级队列)实现的定时器完整代码👇

课件内容:实现定时器

基于“时间轮”的定时器


书接上文:Java EE:2.多线程-初阶(第八弹)~~

 8.4定时器

定时器是什么

定时器类似“闹钟”,时间到了,执行一些逻辑~~

闲聊:定时器这东西各种语言都有~~

但是C语言除外,C语言摆烂太多年了,能提供的基础设施非常有限,就连遍历个目录啥的,C语言自身都做不到,因此肯汤姆逊才去搞了个Go,弥补一些不足~~

定时器是一种实际开发中非常常用的组件

比如网络通信中,如果对方500ms内没有返回数组,则断开连接尝试重连

再比如说一个Map,希望里面的某个Key在3s之后过期(自动删除),也都需要定时器

标准库中的定时器

标准库中提供了一个 Timer 类,Timer类的核心方法为 schedule

schedule 包含两个参数,第一个参数指定即将要执行的任务代码,第二个参数指定多长时间之后执行(单位为毫秒)

package thread;

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

public class Demo37 {
    public static void main(String[] args) {
        Timer timer=new Timer();
        //这个任务会在2000毫秒后执行
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("hello timer");
            }
        },2000);
        System.out.println("hello main");
    }
}

2s钟后的效果👇

我们正常描述任务,是 Runnable

在定时器这里稍微特殊一点,把 Runnable 封装成了 TimerTask

我们进入 TimerTask 内部查看源码会发现,其实还是继承了 Runnable,核心还是重写 run 方法👇

当然,定时器中不只可以添加一个任务,也可以添加多个任务👇

package thread;

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

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

可以看到,三个任务在依次执行👇

答疑:
Q1:程序好像也没有正常结束

因为和线程池一样,Timer中也包含前台线程,阻止进程结束

Q2:抽象类不是不能创建对象吗??

咱们这个不是抽象类,咱这个是匿名内部类~~

因为咱 new 的不是 TimerTask ,咱 new 的是 TimerTask 的子类,只不过这个子类的名字叫什么?不知道,是一个匿名的,这个匿名内部类的写法在一开始讲线程的创建方法时就说过了👉Java EE:2.多线程-初阶(第一弹)

这个代码做了三件事:

1.创建了子类(父类是 TimerTask),子类是匿名的

2.重写了 run

3.new 了子类的实例

实现定时器

1.创建一个类,表示一个任务
2.定时器中,能够管理多个任务的,必须使用一些集合类把这多个任务给管理起来
思考:那什么集合类来管理呢?

ArrayList❌

当管理多个任务的时候,需要确保,时间最早的任务,最下执行

用 ArrayList 就要通过遍历的方式来找到时间最早,就麻烦了

哈希❌

每个任务都是带时间的,我们就需要根据时间来找到任务,哈希也不太方便

队列❌

普通的队列肯定不行,因为通过上述代码能发现,我们添加任务的顺序和执行任务的顺序不一定一样,添加的时候是3→2→1的顺序,执行的时候就是1→2→3

优先级队列

我们可以根据时间来定义优先级,定义一个小根堆,让时间小的跑到队首去

阻塞队列❓

阻塞队列也提供了带优先级的版本,要不要使用,咱后续再说~~

当写到这里时,我们说它是不完整的,因为必须要明确比较规则

如果你的元素是 数字 / String 这种本身就有明确比较规则的对象,可以不额外指定

但现在是把自己定义的类,传进去

下面用实现 Comparable 的 compareTo 方法进行实现

(对于Comparable、Comparator不明白的可以参考:Comparator、Comparable接口及compare、compareTo总结

    @Override
    public int compareTo(MyTimerTask o) {
        //我们的需求是让时间最小的元素在队首
        //=0,说明this和o相等
        //<0,说明this<o
        //>0,说明this>o
        //数学上比较两个数字的大小:作差
        //下面这种情况谁减谁??
        return (int)(this.time-o.time);
        return (int)(o.time-this.time);
        //千万不要背!!背就一定会出错!!
        //咱就做实验,一个不行换另一个
    }
3.实现 schedule 方法,把任务添加到队列中即可

将前三步搞定后的代码👇

package thread;

//写法一:基于抽象类的方式定义 MyTimerTask
//这样的定义虽然可以,但写起来有点麻烦
//abstract class MyTimerTask implements Runnable{
//    @Override
//    public abstract void run();//后续再实现run的逻辑
//}

import java.util.PriorityQueue;

//写法二:持有成员 task 的方式
class MyTimerTask implements Comparable<MyTimerTask>{
    private Runnable task;
    //记录任务要执行的时刻
    private long time;
    public MyTimerTask(Runnable task,long time){
        this.task=task;
        this.time=time;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        //我们的需求是让时间最小的元素在队首
        //=0,说明this和o相等
        //<0,说明this<o
        //>0,说明this>o
        //数学上比较两个数字的大小:作差
        //下面这种情况谁减谁??
        return (int)(this.time-o.time);
        //return (int)(o.time-this.time);
        //千万不要背!!背就一定会出错!!
        //咱就做实验,一个不行换另一个
    }
}
//自己实现一个定时器
class MyTimer{
    private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
    //任务 task 以当前时刻为基准,delay 毫秒后执行
    public void schedule(Runnable task,long delay){
        //创建一个任务对象
        //其中System.currentTimeMillis()是获取当前时刻时间戳的API,返回的是一个long,毫秒级别的时间戳
        MyTimerTask t=new MyTimerTask(task,System.currentTimeMillis()+delay);
        //将任务对象放入任务队列
        queue.offer(t);//不是阻塞队列,不用put
    }
}
public class Demo38 {
    public static void main(String[] args) {

    }
}
4.额外创建一个线程,负责执行队列中的任务

注意:这个和线程池不同,线程池是  只要队列不为空,就立即取任务并执行

此处需要看队首元素的时间,是否到了,时间到→才能执行,时间不到→不能执行

细节:此处我们叫“时刻”,而不是“时间”

时间:时间段(比如1个小时5分钟)

时刻:一个瞬间

计算机中,使用时间戳来表示“时刻”

啥是时间戳??

我们平常所看到的时间,都是格式化时间,是给人看的👇

给计算机看的,叫时间戳👇

这个时间戳是以1970年1月1日0时0分0秒作为基准时刻,计算当前时刻和基准时刻的秒数(毫秒/微秒)之差

当时我们学C语言的时候,讲到 if 的时候,写了猜数字=>随机数=>使用时间戳作为随机种子


package thread;

//写法一:基于抽象类的方式定义 MyTimerTask
//这样的定义虽然可以,但写起来有点麻烦
//abstract class MyTimerTask implements Runnable{
//    @Override
//    public abstract void run();//后续再实现run的逻辑
//}

import java.util.PriorityQueue;

//写法二:持有成员 task 的方式
class MyTimerTask implements Comparable<MyTimerTask>{
    private Runnable task;
    //记录任务要执行的时刻
    private long time;
    public MyTimerTask(Runnable task,long time){
        this.task=task;
        this.time=time;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        //我们的需求是让时间最小的元素在队首
        //=0,说明this和o相等
        //<0,说明this<o
        //>0,说明this>o
        //数学上比较两个数字的大小:作差
        //下面这种情况谁减谁??
        return (int)(this.time-o.time);
        //return (int)(o.time-this.time);
        //千万不要背!!背就一定会出错!!
        //咱就做实验,一个不行换另一个
    }
    public long getTime(){
        return time;
    }
    public void run(){
        task.run();
    }
}
//自己实现一个定时器
class MyTimer{
    private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
    //任务 task 以当前时刻为基准,delay 毫秒后执行
    public void schedule(Runnable task,long delay){
        //创建一个任务对象
        //其中System.currentTimeMillis()是获取当前时刻时间戳的API,返回的是一个long,毫秒级别的时间戳
        MyTimerTask t=new MyTimerTask(task,System.currentTimeMillis()+delay);
        //将任务对象放入任务队列
        queue.offer(t);//不是阻塞队列,不用put
    }
    //构造方法
    public MyTimer(){
        //创建一个线程,负责执行队列中的任务
        Thread t=new Thread(()->{
            while(true){
                //先判定队列是否为空
                if(queue.isEmpty()){
                    continue;
                }
                //取出队首元素
                MyTimerTask task=queue.peek();
                if(System.currentTimeMillis()<task.getTime()){
                    //当前任务时间,如果比系统时间大,说明任务执行的时机未到
                    continue;//先不执行
                }else{
                    //时间到了,执行任务
                    task.run();
                    queue.poll();
                }
            }
        });
        t.start();
    }
}
public class Demo38 {
    public static void main(String[] args) {

    }
}

写到这里时,我们是没有考虑线程安全问题的

当前调用 schedule 是一个线程,定时器内部又有一个线程,多个线程操作同一个队列,一定涉及到线程安全问题

下面我们给上述代码进行加锁👇

①先创建个锁对象:
    //直接使用 this 作为锁对象,当然也是Ok的,这里我们就创建新的对象处理了
    private Object locker=new Object();//加锁处理,创建锁对象

如果使用 this 加锁,this.wait()、this.notify()效果是一样的~~

答疑:在这的两个 this 是一个吗?一个不是内部类的吗?

lambda 也是能捕获到外部类的 this 的

虽然看起来这个 this 是指向匿名内部类,但实际上我们写的是一个 lambda,捕获到的是外部类的 this,而不是内部类的 this

所以也证明了 变量捕获不是只能捕获常量,this指向的是不能修改的,当然也能捕获~~

②给 schedule 加锁:
    //任务 task 以当前时刻为基准,delay 毫秒后执行
    public void schedule(Runnable task,long delay){
        synchronized (locker){
            //创建一个任务对象
            //其中System.currentTimeMillis()是获取当前时刻时间戳的API,返回的是一个long,毫秒级别的时间戳
            MyTimerTask timerTask=new MyTimerTask(task,System.currentTimeMillis()+delay);
            //将任务对象放入任务队列
            queue.offer(timerTask);//不是阻塞队列,不用put
        }
    }

其实写成这样也可以👇,但是写到外面的话可能会因为 synchronized 阻塞,使得时间可能会存在一点误差

    //任务 task 以当前时刻为基准,delay 毫秒后执行
    public void schedule(Runnable task,long delay){
        //创建一个任务对象
        //其中System.currentTimeMillis()是获取当前时刻时间戳的API,返回的是一个long,毫秒级别的时间戳
        MyTimerTask timerTask=new MyTimerTask(task,System.currentTimeMillis()+delay);
        synchronized (locker){
            //将任务对象放入任务队列
            queue.offer(timerTask);//不是阻塞队列,不用put
        }
    }

因为 timerTask 是马上就能执行到(获取了一次系统时间),然后可能在获取锁的时候耽误一点时间,导致锁内部的时间与刚刚获取到的时间不一致,因此我们就以入队列这个时刻作为时间基准了,用锁全包起来

③给构造对象 MyTimer 里的线程加锁

答疑:给构造方法加锁??

构造方法本身可以写 synchronized ,但是这个地方不能这么写,因为要保护的逻辑在线程的 run 里头(lambda),和构造方法是两个不同的方法~~

    //构造方法
    public MyTimer(){
        //创建一个线程,负责执行队列中的任务
        Thread t=new Thread(()->{
            while(true){
                synchronized (locker){
                    //先判定队列是否为空
                    if(queue.isEmpty()){
                        continue;
                    }
                    //取出队首元素
                    MyTimerTask task=queue.peek();
                    if(System.currentTimeMillis()<task.getTime()){
                        //当前任务时间,如果比系统时间大,说明任务执行的时机未到
                        continue;//先不执行
                    }else{
                        //时间到了,执行任务
                        task.run();
                        queue.poll();
                    }
                }
            }
        });
        t.start();
    }

此时我们发现上述4个步骤就写的差不多了👇

package thread;

//写法一:基于抽象类的方式定义 MyTimerTask
//这样的定义虽然可以,但写起来有点麻烦
//abstract class MyTimerTask implements Runnable{
//    @Override
//    public abstract void run();//后续再实现run的逻辑
//}

import java.util.PriorityQueue;

//写法二:持有成员 task 的方式
class MyTimerTask implements Comparable<MyTimerTask>{
    private Runnable task;
    //记录任务要执行的时刻
    private long time;
    public MyTimerTask(Runnable task,long time){
        this.task=task;
        this.time=time;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        //我们的需求是让时间最小的元素在队首
        //=0,说明this和o相等
        //<0,说明this<o
        //>0,说明this>o
        //数学上比较两个数字的大小:作差
        //下面这种情况谁减谁??
        return (int)(this.time-o.time);
        //return (int)(o.time-this.time);
        //千万不要背!!背就一定会出错!!
        //咱就做实验,一个不行换另一个
    }
    public long getTime(){
        return time;
    }
    public void run(){
        task.run();
    }
}
//自己实现一个定时器
class MyTimer{
    private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
    //直接使用 this 作为锁对象,当然也是Ok的,这里我们就创建新的对象处理了
    private Object locker=new Object();//加锁处理,创建锁对象

    //任务 task 以当前时刻为基准,delay 毫秒后执行
    public void schedule(Runnable task,long delay){
        synchronized (locker){
            //以入队列这个时刻作为时间基准
            //创建一个任务对象
            //其中System.currentTimeMillis()是获取当前时刻时间戳的API,返回的是一个long,毫秒级别的时间戳
            MyTimerTask timerTask=new MyTimerTask(task,System.currentTimeMillis()+delay);
            //将任务对象放入任务队列
            queue.offer(timerTask);//不是阻塞队列,不用put
        }
    }
    //构造方法
    public MyTimer(){
        //创建一个线程,负责执行队列中的任务
        Thread t=new Thread(()->{
            while(true){
                synchronized (locker){
                    //先判定队列是否为空
                    if(queue.isEmpty()){
                        continue;
                    }
                    //取出队首元素
                    MyTimerTask task=queue.peek();
                    if(System.currentTimeMillis()<task.getTime()){
                        //当前任务时间,如果比系统时间大,说明任务执行的时机未到
                        continue;//先不执行
                    }else{
                        //时间到了,执行任务
                        task.run();
                        queue.poll();
                    }
                }
            }
        });
        t.start();
    }
}
public class Demo38 {
    public static void main(String[] args) {

    }
}
④接下来我们在 main 方法里写个方法验证一下定时器效果
public class Demo38 {
    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);
    }
}

我们看起来代码确实是能够正常执行了👇

其实我们的代码写到这儿,还有一严重的问题,需要进一步考虑:

一旦这个程序跑起来,电脑风扇就会狂转~~

因为 CPU温度升高=>CPU在高负荷工作,引起这个问题的核心原因就在下述代码👇

        //创建一个线程,负责执行队列中的任务
        Thread t=new Thread(()->{
            while(true){
                synchronized (locker){
                    //先判定队列是否为空
                    if(queue.isEmpty()){
                        continue;
                    }
                    //取出队首元素
                    MyTimerTask task=queue.peek();
                    if(System.currentTimeMillis()<task.getTime()){
                        //当前任务时间,如果比系统时间大,说明任务执行的时机未到
                        continue;//先不执行
                    }else{
                        //时间到了,执行任务
                        task.run();
                        queue.poll();
                    }
                }
            }
        });

这里的转,其实是在等待新的任务到来,明明是等,却需要消耗大量的CPU资源,这是不太科学的

这种通过CPU不停的死循环式等待,在计算机中称为“忙等”(忙了一天,又没啥实质性的产出~~)

如何解决呢??

sleep❌

如果设置的短,起不到解决忙等的效果

如果设置的长,万一有新的任务来了,又没法及时处理~~(如果你 sleep(2000),这时候你 schedule 1000,那这个 1000 的就响应不了了)

wait notify

这个才是正确的解决方式,我们可以按需等待,按需唤醒~~

我们只需要去 wait 就可以了,等到其他有线程添加任务的时候再去把它唤醒~~

    //构造方法
    public MyTimer(){
        //创建一个线程,负责执行队列中的任务
        Thread t=new Thread(()->{
            try{//将wait的异常处理放到最外面了
                while(true){
                    synchronized (locker){
                        //先判定队列是否为空
                        while(queue.isEmpty()){//换成wait之后,这里要把if改成while,防止出现Bug
                            //这里的 sleep 时间不好设定!!
                            //Thread.sleep();
                            //continue;
                            locker.wait();
                        }
                        //取出队首元素
                        MyTimerTask task=queue.peek();
                        if(System.currentTimeMillis()<task.getTime()){
                            //当前任务时间,如果比系统时间大,说明任务执行的时机未到
                            continue;//先不执行
                        }else{
                            //时间到了,执行任务
                            task.run();
                            queue.poll();
                        }
                    }
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        });
        t.start();
    }

再写唤醒操作:队列不为空,就唤醒~~

    public void schedule(Runnable task,long delay){
        synchronized (locker){
            //以入队列这个时刻作为时间基准
            //创建一个任务对象
            //其中System.currentTimeMillis()是获取当前时刻时间戳的API,返回的是一个long,毫秒级别的时间戳
            MyTimerTask timerTask=new MyTimerTask(task,System.currentTimeMillis()+delay);
            //将任务对象放入任务队列
            queue.offer(timerTask);//不是阻塞队列,不用put
            locker.notify();//唤醒操作
        }
    }

在处理完 忙等 的问题之后,其实还有一处问题:就出现在这儿👇

假设当前时刻是14:00,队首的任务时刻是14:30,时间还没到,在上述代码中就会执行continue,啥也不干,直接回到循环那里,继续执行 while 判断,继续执行 wait ,继续执行 if 判断,意味着这整个逻辑又会出现快速的循环👇

这就相当于我中午睡觉,14:30起床,闹钟响了,一看:14:00,好,我趴下,我再起来看:14:00,好,我再趴下,我再起来看:14:00,好,我再趴下,我再起来看:14:01……那我这个觉就睡了个寂寞,全在看时间呢,也属于忙等,这个过程虽然也是在等,但我也没闲着,完全起不到任何休息的作用

答疑:用 goto

goto 是汇编(机器指令)无条件跳转指令~~Java中没有 goto ,其他语言(C/C++)中的 goto 都被喷成🐶了,因为 goto 会搞出很多骚操作~~一个代码中如果有很多 goto ,代码就会变成意大利面条~~

因此我们也需要用到 wait ,直接这么 wait 吗👇??

其实这并不合适,因为这个 wait 必须得有其他线程 schedule 才能唤醒,实际上,此处是应该时间到,就继续执行的!!!

因此我们不能用 wait 死等,而是给 wait 设置超时时间,这个超时时间就是时间差👇

if(System.currentTimeMillis()<task.getTime()){
    //当前任务时间,如果比系统时间大,说明任务执行的时机未到
    //continue;//先不执行
    locker.wait(task.getTime()-System.currentTimeMillis());

比如当前时刻是14:00,队首的任务时刻是14:30,那么此处就是 wait 30分钟

此时我们思考另一个问题:比如 wait 30分钟,如果才过了10分钟(14:10),恰好有一个新的任务来了,调用 schedule ,添加了15:00的任务,那么 schedule 中的 notify 不就把这里的 wait 给唤醒了吗?(相当于才休息10分钟,就被唤醒了~~)(notify 不是只能唤醒不带参数的 wait ,而是都能唤醒的~~)

其实这个无关紧要的~~无非就是再次判定队首元素的任务时间(14:30)和当前时刻(14:10),再次 wait 20分钟呗~~(多执行一次循环,无所谓,主要是怕短时间内进行大量的循环)

答疑:用 sleep 可以吗??

绝对不行的!比如说,本来队首是14:30分,咱们在14:10分添加一个新的任务14:20来执行

如果刚才是 sleep ,sleep 30分钟,无法被唤醒,意味着 14:20的任务(新任务)无法及时执行了,而且 sleep 的时候,不会释放锁的!!(sleep 抱着锁睡着了~~)

这就导致另一个线程调用 schedule 阻塞在加锁的逻辑上~~


闲聊:

1.多线程考虑真多

那确实,因为咱们是要在无数种情况下都要打败灭霸~~势必要多思考~~

2.随便写写就是线程安全问题

不充分理解线程安全,很难正确编写多线程程序

正因为多线程这么难,有的语言直接就把多线程给干掉了!!!

JS:JS只是提供了“定时器”,凑合实现一些并发效果

Python:虽然提供了线程,但Python的线程是“假的线程”

Python的全局解释器锁,使得Python的多线程本质上是“串行执行”的

3.JS的异步操作呢?

底层是多线程,但是封装起来了,让你无法灵活操作

只能 async、await ,操作简单,不容易出错~~


答疑:那这段代码👇的 if 不改成 while 吗?这里也是有 wait 的呀

这里就不建议使用 while 了,虽然 wait 和 while 不分家,但也要分情况

使用 while 就是为了“再次确认,条件是否满足”

当前 wait 唤醒之后,通过总的 while 循环刚好就是再走一遍确认逻辑~~

而且,这里把 if 改成 while 之后,你让下面的 else 情何以堪~~

但是一开始的 while 还是要加的👇

因为下面的操作就开始操作队列了,队列为空,那不就麻烦了嘛,是要出大问题的~~

闲聊:终于知道业务时大部分用 wait 而不是 sleep了

确实,wait 比 sleep 更多

基于堆(优先级队列)实现的定时器完整代码👇
package thread;
//自己基于堆(优先级队列)实现的定时器
//写法一:基于抽象类的方式定义 MyTimerTask
//这样的定义虽然可以,但写起来有点麻烦
//abstract class MyTimerTask implements Runnable{
//    @Override
//    public abstract void run();//后续再实现run的逻辑
//}

import java.util.PriorityQueue;

//写法二:持有成员 task 的方式
class MyTimerTask implements Comparable<MyTimerTask>{
    private Runnable task;
    //记录任务要执行的时刻
    private long time;
    public MyTimerTask(Runnable task,long time){
        this.task=task;
        this.time=time;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        //我们的需求是让时间最小的元素在队首
        //=0,说明this和o相等
        //<0,说明this<o
        //>0,说明this>o
        //数学上比较两个数字的大小:作差
        //下面这种情况谁减谁??
        return (int)(this.time-o.time);
        //return (int)(o.time-this.time);
        //千万不要背!!背就一定会出错!!
        //咱就做实验,一个不行换另一个
    }
    public long getTime(){
        return time;
    }
    public void run(){
        task.run();
    }
}
//自己实现一个定时器
class MyTimer{
    private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
    //直接使用 this 作为锁对象,当然也是Ok的,这里我们就创建新的对象处理了
    private Object locker=new Object();//加锁处理,创建锁对象

    //任务 task 以当前时刻为基准,delay 毫秒后执行
    public void schedule(Runnable task,long delay){
        synchronized (locker){
            //以入队列这个时刻作为时间基准
            //创建一个任务对象
            //其中System.currentTimeMillis()是获取当前时刻时间戳的API,返回的是一个long,毫秒级别的时间戳
            MyTimerTask timerTask=new MyTimerTask(task,System.currentTimeMillis()+delay);
            //将任务对象放入任务队列
            queue.offer(timerTask);//不是阻塞队列,不用put
            locker.notify();//唤醒操作
        }
    }
    //构造方法
    public MyTimer(){
        //创建一个线程,负责执行队列中的任务
        Thread t=new Thread(()->{
            try{//将wait的异常处理放到最外面了
                while(true){
                    synchronized (locker){
                        //先判定队列是否为空
                        while(queue.isEmpty()){//换成wait之后,这里要把if改成while,防止出现Bug
                            //这里的 sleep 时间不好设定!!
                            //Thread.sleep();
                            //continue;
                            locker.wait();
                        }
                        //取出队首元素
                        MyTimerTask task=queue.peek();
                        if(System.currentTimeMillis()<task.getTime()){
                            //当前任务时间,如果比系统时间大,说明任务执行的时机未到
                            //continue;//先不执行
                            locker.wait(task.getTime()-System.currentTimeMillis());
                        }else{
                            //时间到了,执行任务
                            task.run();
                            queue.poll();
                        }
                    }
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        });
        t.start();
    }
}
public class Demo38 {
    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);
    }
}

标准库提供的 Timer 和自己写的 MyTimer 差不多

都是使用一个线程,负责扫描队首元素,并执行的

①如果任务少/任务时间分散,都无所谓

②如果任务特别多/时间非常集中呢?一个线程就可能执行不过来~~

因此我们也需要结合线程池来使用~~

比如我一口气注册了10000个任务,每个任务都是14:00来执行的,此时就完全可以创建多个线程,负责执行这里的队列中的任务~~
一个线程负责扫描,扫描到需要执行的任务,添加到另一个线程池的任务队列中,由多个线程负责执行~~

这样的做法,在Java标准库中也是由体现的,就是这样的代码👇

Executors.newScheduledThreadPool(4);

这样的操作,就是创建了一个带有线程池的定时器

课件内容:实现定时器

定时器的构成

①一个优先级队列(不要使用 PriorityBlockingQueue,容易死锁!)

②队列中的每个元素是一个 Task 对象

③Task 中带有一个时间属性,队首元素就是即将要执行的任务

④同时有一个 worker 线程一直扫描队首元素,看队首元素是否需要执行

1.Timer 类提供的核心接口为 schedule ,用于注册一个任务,并指定这个任务多长时间后执行

public class MyTimer {
    public void schedule(Runnable command, long after) {
        // TODO
    }
}

2.Task 类用于描述一个任务(作为 Timer 的内部类),里面包含一个 Runnable 对象和一个 time (毫秒时间戳)

这个对象需要放到 优先级队列 中,因此需要实现 Comparable 接口

class MyTask implements Comparable<MyTask> {
    public Runnable runnable;
    // 为了方便后续判定, 使用绝对的时间戳.
    public long time;

    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        // 取当前时刻的时间戳 + delay, 作为该任务实际执行的时间戳
        this.time = System.currentTimeMillis() + delay;
    }

    @Override
    public int compareTo(MyTask o) {
        // 这样的写法意味着每次取出的是时间最小的元素.
        // 到底是谁减谁?? 俺也记不住!!! 随便写一个, 执行下, 看看效果~~
        return (int)(this.time - o.time);
    }
}

3.Timer 实例中,通过 PriorityQueue 来组织若干个 Task 对象

通过 schedule 来往队列中插入一个个 Task 对象

class MyTimer {
    // 核心结构
    private PriorityQueue<MyTask> queue = new PriorityQueue<>();
    // 创建一个锁对象
    private Object locker = new Object();

    public void schedule(Runnable command, long after) {
        // 根据参数, 构造 MyTask, 插入队列即可.
        synchronized (locker) {
            MyTask myTask = new MyTask(runnable, delay);
            queue.offer(myTask);
            locker.notify();
        }
    }
}

4.Timer 类中存在一个 worker 线程,一直不停的扫描队首元素,看看是否能执行这个任务

所谓“能执行”指的是该任务设定的时间已经到了

// 在这里构造线程, 负责执行具体任务了.
public MyTimer() {
    Thread t = new Thread(() -> {
        while (true) {
            try {
                synchronized (locker) {
                    // 阻塞队列, 只有阻塞的入队列和阻塞的出队列, 没有阻塞的查看队首元素.
                    while (queue.isEmpty()) {
                        locker.wait();
                    }
                    MyTask myTask = queue.peek();
                    long curTime = System.currentTimeMillis();
                    if (curTime >= myTask.time) {
                        // 时间到了, 可以执行任务了
                        queue.poll();
                        myTask.runnable.run();
                    } else {
                        // 时间还没到
                        locker.wait(myTask.time - curTime);
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t.start();
}
基于“时间轮”的定时器

定时器,除了基于堆(优先级队列)方式来实现的定时器之外,还有一种方案,基于“时间轮”

①类似搞个循环队列(数组)

②每个元素是一个“时间单位”

③每个元素又是一个链表

④每到一个时间单位,光标指向下一个元素,同时把这个元素上对应链表中的任务都执行一遍……

优势:性能更高(就是往下走一个格,速度很快),更适合任务特别多的情况

劣势:时间精度不如优先级队列(优先级队列,最大的问题在于堆的调整,是logN的,因此更适合精度高的情况)

重点还是在于掌握优先级队列的定时器:

①代码中涉及到的锁操作、wait-notify操作

②忙等 部分的讨论

由于定时器,是一个非常重要的组件,因此会在分布式系统中,把定时器专门提取出来,封装成一个单独的服务器(和消息队列很像)

分布式系统,有很多服务器

如果只是一个类,意味着所有的服务器都需要执行这一套同样的逻辑,比较复杂,如果进行整改,所有的都要改

因此提取出来,形成独立的服务器,大家都去调用,如果未来有升级调整,也便于分配给这个定时任务服务器单独的硬件资源~~

闲聊:

简历上不是“烂大街”的项目不能写,而是你“一知半解”就往上写~~
如果有同学真的能把这个项目吃透,那么面试官会认为这个同学完全Ok的

1.愿意主动学习

2.自学能力很强

如果你简历上写一堆东西,结果让你介绍一下:

项目的登录逻辑是怎样实现的

会话是如何存储的

会话过期策略是怎样设定的

……

你答不上,面试官只能认为,你学的火候太差了~~

像这种技术面试,面试官是可以通过简单的几个问题,就摸出你的老底儿~~

答疑:

Q1:一般做项目是1~2个吗?

多多益善,能做10个8个的最好不过了~~多一个项目,多一分容错~~

项目多了,更大概率打动面试官(每个项目都得认真做,面试官绝对不傻,不好糊弄~~)

Q2:到大三也做不了这么多啊

10个8个确实有点夸张,搞个3个、4个、5个还是很有可能的~~在于数量、质量

Q3:啥是微服务??

在后续学完Java EE进阶后,就会去接触 Spring Cloud(微服务),在Java圈子里,谈到微服务,基本等价于 Spring Cloud,到时候会学一些 nacos、openfeign、gateway操作……

微服务不是微信服务,是一个“噱头”

咱们同学以后工作中,大部分同学是接触不到微服务的

微服务只有中大厂才会涉及到

对于大部分的中小公司来说,单机程序/简单的分布式系统 足以应付业务场景

服务器,最初,一个服务器程序处理所有工作

后来分成3、4个服务器配合=>分布式

后来分成20个、30个服务器配合=>微服务

中大厂这种规模比较大的团队,微服务才能真正发挥威力~~

目前很多同学把“微服务”奉为至宝、吊炸天的技术~~

殊不知,任何技术,确实能解决问题,也都要付出代价~

更多推荐