多线程代码案例(定时器) - 3

定时器,是我们日常开发所常用的组件工具,类似于闹钟,设定一个时间,当时间到了之后,定时器可以自动的去执行某个逻辑

目录

[Timer 的基本使用](#Timer 的基本使用)

[实现一个 Timer](#实现一个 Timer)

通过这个类,来描述一个任务

通过这个类,来表示一个定时器

[MyTimer 构造方法,创建扫描线程,让扫描线程来完成判定和执行](#MyTimer 构造方法,创建扫描线程,让扫描线程来完成判定和执行)

[问题一: 线程安全问题](#问题一: 线程安全问题)

[第二个问题:如果一直执行 while 那就一直在加锁和解锁...](#第二个问题:如果一直执行 while 那就一直在加锁和解锁...)

完整代码如下:

流程图:

执行顺序解析:

注意:


Timer 的基本使用

Java 标准库中,也提供了定时器的实现。

创建一个 timer 对象之后,调用 timer 的schedule 方法,在schedule 方法中,参数是一个匿名内部类,重写 run 方法,run 方法的方法体中,就是我们时间到了之后要执行的代码,还有一个参数是我们要等待的时间(单位是ms)(这里不可以使用 lambda 表达式。lambda 表达式得是函数时接口才行,即 interface 里面只能有一个方法)

定义一个 timer 添加多个任务,多个任务同时会带有一个时间:

打印结果如下:

且当打印完成之后,进程并不会结束,Timer 里内置了一个线程(前台线程)

timer 并不知道我们的代码是否还会添加新的任务进来,处在"严阵以待"的状态,我们需要使用 cancel 来主动结束,否则 Timer 是不知道是否其他地方还要继续添加任务的。

实现一个 Timer

Timer 里面要包含那些内容呢?

  1. 需要一个线程,负责帮我们来掐时间。等任务到达合适的时间,这个线程就负责执行。

  2. 还需要一个队列 / 数组,能够保存所有 schedule 进来的任务。

直观想,如果这个线程,不停的去扫描上述队列中的每个元素,看每个任务的时间否达到,到时间就执行。(但如果这个队列很长,这个遍历的过程的开销就很大了 O(N) )

==》优先级队列!!!

每个任务都是带有 delay 时间的,一定是先执行时间小的,后执行时间大的。就不需要对上述队列进行遍历了,只需要关注队首元素是否到时间。(如果队首没到时间,后续其他元素也就一定没到时间) ==》 可以使用标准库提供的PriorityQueu(线程不安全),也有 PriorityBlockingQueue(线程安全)(但在我们此处的场景中,PriorityBlockingQueue 不太好控制,容易出问题)(我们可以对 PriorityQueue进行手动加锁,确保线程安全)

开始实现:

通过这个类,来描述一个任务

时间戳:以 1970 年 1 月 1 日 0 时 0 分 0 秒,为基准,计算当前时刻和基准时刻的 秒数 / 毫秒数 / 微秒数...

delay 是一个"相对"的时间间隔,也就是以当前时刻为基准,计算 3000 ms 之后才执行。

针对上面的类,还有什么比较重要的问题呢???==》 我们上面的类,是要放在优先级队列中的。有些集合类,是对元素有特定要求的(PriorityQueu TreeMap TreeSet 都是要求元素是"可比较大小的" ==》 Comparable,Comparator)(HashMap,HashSet 则是要求元素是"可比较相等" "可 hash 的"==》 equals hashCode)

实现 Comparable 接口,重写 compareTo 方法

此处是期望根据时间,时间的作为优先级更高...(当我们要使用 compareTo 的时候,千万不要背到底是那个 - 那个,直接写代码试一试即可!!! ==》 作为程序员,要"扬长避短",记忆这个事情,并不是我们人脑擅长的...)

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

    //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);
    }
    
    public long getTime() {
        return time;
    }
}

通过这个类,来表示一个定时器

其中定义了 t 线程,负责扫描任务队列,执行任务。

用优先级队列创建一个 queue,其中元素存放 MyTimer(我们刚刚创建的任务类)

在 schedule 方法中,传入两个参数 runnable 和 delay,将 task 添加入 queue 当添加成功的时候, notify 锁

MyTimer 构造方法,创建扫描线程,让扫描线程来完成判定和执行

当前这个代码,还至少有两个核心问题,需要解决。

问题一: 线程安全问题

在主线程中,我们 new 新的 Timer 然后调用 schedule 方法来添加任务。

在 MyTimer 中,我们 new 了个优先级队列 queue 作为成员变量,但优先级队列是线程不安全的,需要进行加锁来解决线程安全问题。

我们是在另一个线程中,即 MyTimer 的构造方法中的 t 线程中,对队列进行删除,应该在这个线程中加锁

在这个线程中,synchronized 加在哪里呢?我们先大致观察,发现 t 线程中,都是写操作,为了要实现我们前面提到的原子性,就可以给整个线程加锁 ==》

但这样加锁,是正确的吗???

我们在主线程中 new 一个 MyTimer 对象

然后进入 MyTimer 的构造方法:

构造方法就是 t 线程,这个线程进来之后,就直接加锁了,加锁之后,才会进入 while 循环,while 循环结束了之后,锁才能释放,但有没有一种可能,我们的 while 循环结束不了,导致我们的锁无法释放...

主线程下面的语句 myTimer 调用 schedule 方法

schedule 方法中也在尝试加锁,但锁被构造方法中占用着,这里就没办法加上锁了...

一番调整之后,我们可以把 synchronized 加在 while 里面,这样才会有释放锁的机会,外面才有可能拿到锁

第二个问题:如果一直执行 while 那就一直在加锁和解锁...

我们上面的代码,当队列为空,没有任务的时候,直接 continue,然后又 while,同样的,时间还没到,直接 continue,然后又 while,这里两个代码继续循环是没有意义的,应该等一等!!!

这里两个代码的执行速度是非常快的,当解锁之后,马上又会进入 while 循环,立即又重新尝试加锁了,导致其他线程想要通过 schedule 加锁,但是加不上(线程饿死

==》 解决方法:引入 wait

注意: 在当时间还没到,暂时不执行中,使用 sleep 是不太合适的。(可能我们会想,当 curTime >= task.getTime() 不满足的时候,那我们 sleep task.getTime() - curTIme 时间即可,但这样是不行的!)

但我们可以写为 locker.wait(task.getTime() - curTime)

  1. wait 的过程中,有新的任务来了,wait 就会被唤醒!!!schedule 有 notify的!会根据新的任务重新计算要等待的时间

  2. wait 的过程中,没有新的任务,时间到了,按照原定计划,执行之前的这个最早的任务即可。

但 wait 我们在前面讲过,wait 可能存在"虚假唤醒"的情况,即在 wait 的等待过程中,发现了一些意料之外的情况,所以前面我们是用 while 来进行多次确定的,这里的话,我们可以再进行一次任务检查。 ==》

这样,我们就成功实现了一个 Timer

完整代码如下:

java 复制代码
import java.util.PriorityQueue;

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

    //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);
    }

    public long getTime() {
        return time;
    }
}

// 通过这个类,来表示一个定时器
class MyTimer {
    // 负责扫描任务队列,执行任务的线程
    private Thread t = null;

    // 任务队列
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

    // 锁对象
    private Object locker = new Object();

    public void schedule(Runnable runnable, long delay) {
        synchronized (locker) {
            MyTimerTask task = new MyTimerTask(runnable, delay);
            queue.offer(task);

            locker.notify();
        }
    }

    // MyTimer 构造方法,用来创建扫描线程,让扫描线程来完成判定和执行
    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 {
                            // 当前时间还没到,暂时先不执行
                            locker.wait(task.getTime() - curTime);
                            if (curTime >= task.getTime()) {
                                queue.poll();
                                task.run();
                            }
                        }
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
    }
}

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

流程图:

执行顺序解析:

注意:

这里的 wait 之后,还需要再跟一个条件判断。

当我们加了再次条件检查之后,就算是被唤醒了之后,但因为有我们的条件检查,(当前时间还未达到任务执行时间),此时代码中虽然没有显示地再次调用 wait 方法让线程继续等待,但由于整个扫描线程的逻辑是在一个无限循环 while(true) 中,在这一轮循环结束后,会进入下一轮循环,在下一轮循环中,会再次检查任务队列的状态

当再次检查的时候,发现 curTine 仍然小于 task.getTime(),就会再次执行 locker.wait(task.getTime() - curTime)让线程继续等待,直到时间到达,或者再次被唤醒并满足条件。所以,虽然在唤醒后没有立即再次调用 wait 方法,但通过循环结构和条件判断,最终还是实现了线程在条件不满足的时候继续等待的逻辑。

还有就算,在我们的代码中,schedule 方法等待的时间,是相对于调用 schedule 方法时的当前时间而言的,在我们的示例代码中,第一个 schedule 方法调用之后,1000ms 之后会输出 "hello 1000",第二个 schedule 方法调用之后,2000ms 之后会输出"hello 2000",第三个 schedule 方法调用之后,3000ms 之后会输出"hello 3000".

但是我们这里的三个 schedule 方法调用的时间几乎是同步,所以差不多就是在执行程序之后的 3s 之后会输出"hello 3000"

相关推荐
一只小松许️34 分钟前
Rust泛型与特性
java·开发语言·rust
星星火柴9362 小时前
数据结构:哈希表 | C++中的set与map
数据结构·c++·笔记·算法·链表·哈希算法·散列表
搬砖工程师Cola3 小时前
<C#>在 C# .NET 6 中,使用IWebHostEnvironment获取Web应用程序的运行信息。
开发语言·c#·.net
CS创新实验室4 小时前
数据结构:最小生成树的普里姆算法和克鲁斯卡尔算法
数据结构·算法·图论·计算机考研
八了个戒5 小时前
「数据可视化 D3系列」入门第三章:深入理解 Update-Enter-Exit 模式
开发语言·前端·javascript·数据可视化
失去妙妙屋的米奇5 小时前
matplotlib数据展示
开发语言·图像处理·python·计算机视觉·matplotlib
夏天的阳光吖5 小时前
C++蓝桥杯实训篇(四)
开发语言·c++·蓝桥杯
angushine6 小时前
Gateway获取下游最终响应码
java·开发语言·gateway
一一Null6 小时前
Token安全存储的几种方式
android·java·安全·android studio
西贝爱学习6 小时前
数据结构:C语言版严蔚敏和解析介绍,附pdf
c语言·开发语言·数据结构