【多线程初阶】多线程案例之定时器

文章目录

  • 前言
  • 1. 什么是定时器
  • 2. 标准库中的定时器
  • 3. 自己实现一个定时器
  • 总结


前言

本文主要给大家讲解多线程的一个重要案例 — 定时器.

关注收藏, 开始学习吧


1. 什么是定时器

定时器也是软件开发中的一个重要组件 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.

【多线程初阶】多线程案例之定时器_第1张图片

  • 定时器是一种实际开发中非常常用的组件, 前端后端开发都会使用到.
  • 比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.
  • 比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除). 类似于这样的场景就需要用到定时器.

2. 标准库中的定时器

  • 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
  • schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).

我们来使用一下标准库中的定时器.

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

public class ThreadDemo20 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        // 给 timer 中注册的这个任务, 不是在调用 schedule 的线程中执行的. 
        // 而是通过 Timer 内部的线程, 来负责执行的.
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 3");
            }
        }, 3000);

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

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

        System.out.println("program start");
    }
}

代码效果

【多线程初阶】多线程案例之定时器_第2张图片
可以看到, hello 1 2 3 按照时间设置依次执行. Timer 内部, 有着自己的线程. 为了保证随时可以处理新安排的任务, 这个线程会持续执行, 并且这个线程是前台线程, 不可以被打断.

接下来我们自己尝试来实现一个定时器

3. 自己实现一个定时器

要想实现一个定时器, 我们需要先想一想定时器其中的主要逻辑. 我们需要用一个类 (TimerTask) 将一个任务给描述出来, 再用一个类 (Timer) 使用数据结构将多个任务给组织起来.

  • 创建一个 TimerTask 这样的类, 用来表示一个任务. 这个任务需要包含两个方面, 任务的内容, 任务需要执行的实际时间. 我们可以使用时间戳来表示, 在 schedule 中, 先获取到当前的系统时间, 在这个基础上, 加上 delay 时间间隔, 就得到了该任务需要执行的实际时间了.
  • 创建一个 Timer 这样的类, 使用合适的数据结构, 把多个任务给组织起来. 在这里我们使用优先级队列 PriorityQueue 来实现是最合适的了, 这样队首元素永远都是第一个需要被执行的任务. 我们还需要一个扫描线程, 在获取到队首元素时间后, 根据差值来决定等待时间, 在这个时间到达之前, 不进行重复扫描, 降低扫描次数.

在实现定时器时, 我们有了以上的逻辑支撑, 还需要注意三个关键问题:

  1. 要保证线程安全, 我们需要给针对 queue 的操作进行加锁.
  2. 在扫描线程中, 不使用 sleep 来进行休眠, 而是用锁操作 wait 来代替. 因为 sleep 进入阻塞后, 不会释放锁, 这样会影响到其余进程, 如 schedule 的添加任务操作. 并且 sleep 在休眠过程中, 不方便提前中断.
  3. 我们放到优先级队列中的元素, 必须是 “可比较的”, 需要通过 Comparable 或者 Compartor 定义任务之间的比较规则. 按照需要被执行的时间顺序来比较.

掌握以上几点后, 我们便可以开始动手实现一个定时器了.

import java.util.PriorityQueue;

class MyTimerTask implements Comparable<MyTimerTask> {

    private Runnable runnable;

    private long time;

    public MyTimerTask (Runnable runnable, long delay) {
        this.runnable = runnable;

        this.time = System.currentTimeMillis() + delay;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public long getTime() {
        return time;
    }

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

class MyTimer {

    // 使用优先级队列, 来保存上述的 N 个任务
    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 中还需要构造一个 "扫描线程", 一方面去负责监控队首元素是否到点了, 是否应该执行; 一方面当任务到点之后,
    // 就要调用这里的 Runnable 的 Run 方法来完成任务
    public MyTimer() {
        // 扫描线程
        Thread thread = new Thread(() -> {
            while (true) {
                try {
                    synchronized (locker) {
                        while (queue.isEmpty()) {
                            // 注意, 当前如果队列为空, 此时就不应该去取这里的元素.
                            // 此处使用 wait 等待更合适.
                            // 如果使用 continue, 就会使这个线程 while 循环运行的飞快,
                            // 也会陷入一个高频占用 cpu 的状态(忙等).
                            // continue;
                            locker.wait();
                        }

                        // 拿出目前需要最早执行的任务
                        MyTimerTask task = queue.peek();
                        long curTime = System.currentTimeMillis();

                        if (curTime >= task.getTime()) {
                            // 假设当前时间是 14:01, 任务时间是 14:00,
                            // 此时就意味着应该要执行这个任务了.
                            queue.poll();
                            task.getRunnable().run();
                        } else {
                            // 让当前扫描线程休眠一下, 按照时间差来进行休眠.
                            // Thread.sleep(task.getTime() - curTime);
                            locker.wait(task.getTime() - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        thread.start();
    }

}

总结

✨ 本文讲解了多线程案例中的一个经典案例, 定时器, 带大家简单了解了一下标准库库中的定时器, 重点需要掌握其核心的逻辑是什么, 如何自己去实现一个定时器.
✨ 想了解更多的多线程知识, 可以收藏一下本人的多线程学习专栏, 里面会持续更新本人的学习记录, 跟随我一起不断学习.
✨ 感谢你们的耐心阅读, 博主本人也是一名学生, 也还有需要很多学习的东西. 写这篇文章是以本人所学内容为基础, 日后也会不断更新自己的学习记录, 我们一起努力进步, 变得优秀, 小小菜鸟, 也能有大大梦想, 关注我, 一起学习.

再次感谢你们的阅读, 你们的鼓励是我创作的最大动力!!!!!

你可能感兴趣的:(多线程学习之路,多线程,java,定时器,案例,开发语言)