多线程(初阶八:计时器Timer)

目录

一、标准库中的计时器

1、计时器的概念

2、计时器的简单介绍

二、模拟实现一个计时器

1、思路

(1)计数器中要存放任务的数据结构

(2)存放优先级队列中的类型:自定义任务类MyTimerTask

(3)计数器类MyTimer

MyTimer类:

MyTimerTask任务类:

2、分析计时器的线程安全问题

(1)维护队列进出的操作

(2)当队列是空的,就要阻塞等待

(3)如果没到时间,就要等待到时在执行要执行的代码


一、标准库中的计时器

1、计时器的概念

计时器类似闹钟,有定时的功能,闹钟是到时间就会响,而计时器是到时间就会执行某一操作,可以指定时间,去执行某一任务(某一代码)。

2、计时器的简单介绍

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

代码演示:

java 复制代码
public class Test1 {
    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");
    }
}

执行结果:

可以看到先打印 hello main ,等过了1s才打印 hello 1000,往后继续推,说明Timer内置了线程,main线程不用等待,而timer类是要到时间才会执行任务代码。注意:这里的线程并没有结束,可以看到idea里也没有显示线程结束,说明timer类里面内置的是前台线程。

但是timer类里面有cancel方法,可以结束线程,我们把这个方法加到打印hello 3000那方法里面,这样就可以结束timer类里面的线程了。

代码:

java 复制代码
public class Test1 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 3000");
                timer.cancel();
            }
        }, 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");
    }
}

执行结果:

可以结束线程。


二、模拟实现一个计时器

1、思路

(1)计数器中要存放任务的数据结构

首先,我们知道,计时器是可以定时去执行一些任务操作,那么我们怎么每次先去执行时间小的那一操作呢?用数组吗?其实在某一些场景下确实可以用数组,但这就需要我们每次都去遍历数组,找出最小的时间,但是如果我们要定时很多任务,成千上万呢?这就不合理了,从数组里面找出这个时间最小的数据,一方面要考虑资源花销大的问题,还有要考虑时间的问题,找的时间太长,错过了已经到时要执行的任务,这说明,使用数组存放任务是不合理的。

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

(2)存放优先级队列中的类型:自定义任务类MyTimerTask

任务类是放要执行的代码和要执行任务时间,单独作为一类,存进优先级队列中,其中,优先级队列里的比较是按任务类的时间大小来比较的。

(3)计数器类MyTimer

里面有一个线程,放在MyTimer类的构造方法中,这个线程就是扫描线程 ,而这个扫描线程来完成判断和操作,入队列或者判断啥时候才执行要执行的代码的操作;还有创建任务schedule的方法,里面也有入队列的操作。

代码:

MyTimer类:
java 复制代码
//通过这个类表示定时器
class MyTimer {
    Object locker = new Object();
    //负责扫描任务队列,执行任务的线程
    private Thread t = null;
    //任务队列
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    //入队列的方法
    public void schedule(Runnable runnable, long delay) {
        synchronized (locker) {
            MyTimerTask task = new MyTimerTask(runnable, delay);
            queue.offer(task);
            locker.notify();
        }
    }
    //构造方法,会创建线程,让扫描线程来完成判定和执行
    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);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

里面的核心代码:schedule方法,这是创建任务,里面包含了要执行的代码和执行代码的时间,还有就是构造方法,里面有一个线程,这个线程就是不断去判断队列有没有任务,到时间了的任务就拿队伍里时间最小的任务,执行这任务里的代码,没到时间就要等。

MyTimerTask任务类:

代码:

java 复制代码
//通过这个类,来描述一个任务
class MyTimerTask implements Comparable<MyTimerTask>{
    //在什么时间点来执行任务
    private long time;//此处的time是一个ms级别的时间戳
    //实际任务要执行的代码
    private Runnable runnable;
    public MyTimerTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        //计算的是要执行任务绝对时间,方便判断是否到达时间了
        this.time = System.currentTimeMillis() + delay;
    }
    //得到要执行任务时间的方法
    public long getTime() {
        return this.time;
    }
    public void run() {
        this.runnable.run();
    }
    @Override
    public int compareTo(MyTimerTask o) {
        return (int)(this.time - o.time);
    }
}

任务类里面放要是和执行的代码,和要执行代码的时间,因为要放进队列里,所以要实现一个比较器,用时间来比较,重写compareTo方法。

2、分析计时器的线程安全问题

(1)维护队列进出的操作

我们知道,不创建其他线程,就一个主线程去调用MyTimer类的话,一共就会有两个线程:主线程和 t 线程,这时候,主线程的代码是这样的

代码:

java 复制代码
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");
    }
}

主线程有入队列的操作,但 t 线程也有出队列的操作,如图:

多线程操作一个队列有进有出,肯定是线程不安全的操作;所以,要维护这个队列,就要把入队列和出队列操作都上锁,同一时间要么只能入队列,要么只能出队列;

入队列操作上锁位置好知道,把创建任务和入队列操作都上锁;但是出队列呢?要在哪里上锁,把while循环都给上锁了?显然,这样的代码感觉有点危险,在这场景上确实可以用,但是,如果是在其他场景下,如果一个线程拿到锁了,但是因为没有实际来执行代码,就会不停的解锁、加锁,这样其他线程就饿死了,所以,还是在while里面,把里面的操作给上锁,这样看着没那么膈应。

如图的代码是最终版本的,上面的代码都是最终版本的。

(2)当队列是空的,就要阻塞等待

如图:

(3)如果没到时间,就要等待到时在执行要执行的代码

没到时间,就要阻塞等待,等待时间是: 要行的时间 - 现在的时间,没有限制要等待的时间的话,就会一直循环,每次循环判断是不是到时间了,因为循环这个代码执行速度是很快的,这样就会盲等,虽然计算机是在忙,都是在瞎忙活,所以代码要写出这样子,如图:


以上的代码都是计时器完整的版本,都看到这了,点个赞再走吧,谢谢谢谢谢!!!

相关推荐
杨充1 分钟前
13.观察者模式设计思想
java·redis·观察者模式
Lizhihao_3 分钟前
JAVA-队列
java·开发语言
喵叔哟12 分钟前
重构代码之移动字段
java·数据库·重构
喵叔哟13 分钟前
重构代码之取消临时字段
java·前端·重构
fa_lsyk15 分钟前
maven环境搭建
java·maven
远望清一色21 分钟前
基于MATLAB边缘检测博文
开发语言·算法·matlab
何曾参静谧30 分钟前
「Py」Python基础篇 之 Python都可以做哪些自动化?
开发语言·python·自动化
Prejudices33 分钟前
C++如何调用Python脚本
开发语言·c++·python
Daniel 大东34 分钟前
idea 解决缓存损坏问题
java·缓存·intellij-idea
wind瑞40 分钟前
IntelliJ IDEA插件开发-代码补全插件入门开发
java·ide·intellij-idea