【JavaEE初阶系列】——多线程案例三——定时器

news2024/11/27 14:55:52

目录

🚩定时器是什么

🚩标准库中的定时器

🚩自定义定时器

🎈构造Task类

📝相对时间和绝对时间

🎈构造MyTime类

📝队列空和队列不为空

📝wait(带参)解决消耗资源问题

📝为什么使用wait,不使用sleep

📝为什么使用PriorityQueue(),不使用PriorityBlockingQueue()

🚩测试

🚩带有解释的完整代码


🚩定时器是什么

日常开发常见组件。约定一个时间,时间到达之后,执行某个代码逻辑。定时器非常常见,尤其在进行网络通信的时候。比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连. 比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除). 类似于这样的场景就需要用到定时器

当客户端发出请求之后,等待响应,如果服务器迟迟没有响应,该怎么办?网络世界是很复杂的,我们也不知道这个请求有没有发过去?响应有没有丢?

对于客户端来说,不能无限的等,需要有一个最大限度,到达最大限度的时候,是重新发一遍,还是彻底放弃,还是其他的方式........

在java的标准库中,都是有现成的定时器实现的


🚩标准库中的定时器

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

public class Test {
    public static void main(String[] args) {
        Timer timer=new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("2s后执行");
            }
        },2000);
        System.out.println("开始执行");
    }
}

这段程序就是创建一个定时器,然后提交一个2000s后执行的任务。

此处使用的是匿名内部类的写法,继承了TimerTask并且创建出了一个实例。这样的目的是为了重写run,通过run描述任务的详细情况。因为TimerTask是个抽象类本身是没有方法的,它存在的意义是继承,本个案例是实现接口Runnable里面的run方法。通过run方法描述任务的详细情况。

另一个参数是相对时间,当前安排的任务,啥时候执行,此处填写的时间,就是以当前时刻为基准,往后再推xxx的时间。

主线程执行schedule方法的时候,就是把这个任务放到timer对象中了,与此同时,timer里头也包含了一个线程,这个线程叫做“扫描线程”,一旦时间到,扫描线程就会执行刚才安排的任务了。仔细观察,可以发现,整个进程其实并没有结束,就是因为Timer内部的线程,阻止了进程结束。等下面实现定时器的时候,我们就可以知道为什么不结束了。


🚩自定义定时器

我们自己实现一个定时器的前提是我们需要弄清楚定时器都有什么:

 Timer timer=new Timer();
  • Timer中需要有一个线程,扫描任务是否到时间,可以执行了。
  • 需要有一个数据结构,把所有的任务都保存起来。
  • 还需要创建一个类,通过类的对象来描述一个任务(至少要包含任务内容和时间)

创建一个扫描线程相对比较简单,我们需要确定一个数据结构来保存我们提交的任务,我们提交过来的任务,是由任务和时间组成的,我们需要构建一个TimeTask对象,数据结构我们这里使用优先级队列,因为我们的任务是有时间顺序的,具有一个优先级,并且要保证在多线程下是安全的,所以我们这里使用:PriorityQueue比较合适。因为Timer中添加的这些任务,都是带有一个“时间”,一定是时间小的先执行,最先执行的就是时间最小的任务,如果时间最小的任务还没到时间,其他任务更不会执行了。(优先级队列,可以使用O(1)时间,来获取到时间最小的任务)。


🎈构造Task类

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

📝相对时间和绝对时间

//执行任务的时间(绝对时间)
private long time;

此时记录的是一个“绝对的时间"(完整的时间戳)。

  • 绝对时间:当前具体的时间
  • 相对时间:时间间隔

schedule方法里面的第二个参数是相对时间,为什么构造的时候记录绝对时间呢?

后续扫描线程的时候,如何判定当前这个任务是否要执行?

  • 获取到当前的时间戳  14:00
  • 再获取到任务要执行的时间戳 14:05
  • 对比俩个时间戳(时间没有到,不能执行)

当前的时间戳是利用到  System.currentTimeMillis()方法,现在我写的时间是20:02,此时时间戳转换成时间就是绝对时间。我们再扫描线程的时候,我们比较的是 当前的时间戳和我要执行的时候的绝对时间,所以我们最好是记录绝对时间。

    public MyTask(Runnable runnable,long delay){
        this.time=System.currentTimeMillis()+delay;
        this.runnable=runnable;
    }

构造方法中的俩个参数是 执行任务的对象以及相对时间。因为再调用schedule方法的时候传的第二个参数是相对时间。我们只需要将 相对时间+时间戳=绝对时间,就可以算出来。

比如现在是20:07的时候,调用的是schedule方法,执行delay是5分钟,那么再对应上面的 System.currentTimeMillis()就是14:00,delay是5分钟,那么绝对时间也就是要执行的时间是14:05分。


当我们创建好属性和获取当前的对象和时间的时候,我们再代码中还有一定的问题。

我们要想到,我们用优先级队列来保存任务,那么用到优先级队列,要求里面的元素务必是可以比较的。所以我们需要实现Comparable接口,继承compareTo方法。

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

由于返回类型是int类型,而时间戳返回类型是long类型,所以我们需要强制类型转换。

我们所创建的任务类里面的成员属性(任务对象和绝对时间)还有构造方法,以及比较方法。为下面的扫描线程铺垫。


🎈构造MyTime类

实现计时器,首先我们要创建一个优先级队列,里面包含的元素是执行的任务,然后实现schedule方法,再调用schedule方法的时候,就插入队列中,而里面的扫描线程应该放在构造方法中,因为我们再创建MyTime类的时候,线程就启动。

class MyTime{
    PriorityQueue<MyTask>queue=new PriorityQueue<>();
    public void schedule(Runnable runnable,long delay){
        queue.offer(new MyTask(runnable,delay));//插入队列中
    }

    public MyTime(){
        Thread thread=new Thread(()->{
            while(true){
                
            }
        });
        thread.start();//放在构造方法中,再创建对象的时候,就启用线程。
    }
}

继续完善代码,


📝队列空和队列不为空

如果队列是空的话,我们就要阻塞等待,等到再调用schedule后插入任务的时候,我们就可以继续执行。我们就会联想到wait()和notify()方法。要想要用wait()就需要加锁。但是我们再上一篇的 阻塞队列中我们知道

wait()再执行的时候要经过三个步骤。

  • 释放锁 ——》前提是先拿到锁
  • 等待通知
  • 通知到来之后,唤醒,重新获得锁

其实我们可以看到我们再创建对象的时候扫描线程启动,当我们调用schedule方法的时候,这也是一个线程再给队列添加元素,不同的线程,再对同一个队列操作,是肯定有线程安全问题的,所以不管使不使用wait()都是得加锁的。

 public void schedule(Runnable runnable,long delay){
        synchronized (this){
            queue.offer(new MyTask(runnable,delay));
            this.notify();//当插入任务后,我们就要唤醒线程
        }
    }

    public MyTime(){
        Thread thread=new Thread(()->{
            while(true){
                try {
                    synchronized (this){
                        while(queue.isEmpty()){
                            this.wait();//如果队列为空,我们就要阻塞等待(队列插入任务)
                        }
                        
                    }
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
        thread.start();
    }

如果队列不为空的情况下

首先我们需要获取当前堆顶的元素(也就是执行时间最先的一个),然后我们需要获取当前的绝对时间(就是你现在再看我的博客的时间),我们需要和任务类里面的time绝对时间(这里的绝对时间是我们要执行任务的时间)相比较,如果当前时间大于等于执行任务时间,那么我们就得执行该任务,执行完后从队列中删除,如果当前时间小于要执行任务的时间,我们就得不执行,等到时间到了为止。

class MyTime{
    PriorityQueue<MyTask>queue=new PriorityQueue<>();
    public void schedule(Runnable runnable,long delay){
        synchronized (this){
            queue.offer(new MyTask(runnable,delay));
            this.notify();
        }
    }

    public MyTime(){
        Thread thread=new Thread(()->{
            while(true){
                try {
                    synchronized (this){
                        while(queue.isEmpty()){
                            this.wait();
                        }
                        MyTask myTask=queue.peek();//获取最先执行的任务(里面有任务对象和绝对时间)
                        long currentTime=System.currentTimeMillis();//获得当前时间
                        if(currentTime>=myTask.getTime()){
                            myTask.getRunnable().run();//执行这个任务
                            queue.poll();//删除堆顶元素
                        }else{

                        }
                    }
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
        thread.start();
    }
}

📝wait(带参)解决消耗资源问题

程序到这里还是有点问题的。

这个程序到这里,比如,我队列中有一个任务,是14:30执行,当时时刻是14:00(时间未到),当时间没到的时候,此处的循环,会快速循环很多次,相当于,时间没到,但是我在这不停的看表,本来这个时间是可以休息的,但是这个不停看表的动作,使我并没有休息~同时,也没有干活。忙等,确实再等,但是也消耗了很多cpu资源,因为没有到时间依旧进入循环。所以此处也加个wait,这里的wait,引入带参数的版本(带有超时时间),把时间间隔作为wait的等待时间了,14:00——14:30 ,此时wait就直接等30min就可以了。

this.wait(myTask.getTime()-System.currentTimeMillis());//执行任务时间-当前时间

而且当任务时间没到的时候,就wiat阻塞(线程不会再cpu上调度,也就把cpu资源让出来给别人了)


📝为什么使用wait,不使用sleep

但是为啥使用wait,不使用sleep行不行? ——wait是更好的

可能在等待过程中,主线程调用schedule添加一个新的任务,新的任务是14:10执行,比刚才最早的任务还早,恰好使用刚才的schedule中的notify就可以唤醒这里的wait,让循环再执行一遍,重新拿到队首的元素(14:10),接下来wait的时间就更新成10min。

 if(currentTime>=myTask.getTime()){
        myTask.getRunnable().run();//执行这个任务
        queue.poll();//队列中执行完了,删除堆顶元素
     }else{
       //当前时间还没到任务时间,暂时不执行任务,但是先啥都不干,等待下一轮的循环判定
          this.wait(myTask.getTime()-System.currentTimeMillis());//执行任务时间-当前时间
     }

📝为什么使用PriorityQueue(),不使用PriorityBlockingQueue()

为什么使用priorityQueue(),其实就是因为要处理俩个wait地方,如果使用阻塞版本的优先级队列,不方便实现这样的俩处等待。

用priorityQueue优先级队列时,一个notify()可以用到俩个wait(),入完任务之后,队列不为空,就唤醒,当最新任务时间还没到的时候,进入阻塞,新的任务来了之后,就唤醒一下,然后重新判定一下最早的任务是啥以及更新等待时间。(所以notify可以同时唤醒俩个wait(),因为入完队列俩个都得唤醒)


🚩测试

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

        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("zyf");
            }
        },3000);

        System.out.println("开始");
    }
}

因为没有任务的时候,代码会一直等待阻塞。 


🚩带有解释的完整代码


import java.util.PriorityQueue;

//首先创建一个类,里面有对象和时间
class MyTask implements Comparable<MyTask>{
    private Runnable runnable;
    //执行任务的时间(绝对时间)
    private long time;
    public MyTask(Runnable runnable,long delay){
        this.time=System.currentTimeMillis()+delay;
        this.runnable=runnable;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public long getTime() {
        return time;
    }

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

class MyTime{
    PriorityQueue<MyTask>queue=new PriorityQueue<>();
    public void schedule(Runnable runnable,long delay){
        synchronized (this){
            queue.offer(new MyTask(runnable,delay));
            this.notify();
        }
    }

    public MyTime(){
        //扫描线程,需要不停的扫描,是否有任务没有完成
        Thread thread=new Thread(()->{
            while(true){
                try {
                    synchronized (this){
                        //不要使用if,作为wait的判断条件,应该使用while
                        //使用while的目的是为了在wait唤醒的时候,再确认一下条件
                        if(queue.isEmpty()){
                            //使用wait进行等待
                            //这里的wait,需要有另外的线程唤醒
                            //添加了新的任务,就应该唤醒
                            this.wait();
                        }
                        MyTask myTask=queue.peek();//获取最先执行的任务(里面有任务对象和绝对时间)
                        long currentTime=System.currentTimeMillis();//获得当前时间
                        //比较一下看当前的队首元素是否可以执行了
                        if(currentTime>=myTask.getTime()){
                            myTask.getRunnable().run();//执行这个任务
                            queue.poll();//队列中执行完了,删除堆顶元素
                        }else{
                            //当前时间还没到任务时间,暂时不执行任务,但是先啥都不干,等待下一轮的循环判定
                            this.wait(myTask.getTime()-System.currentTimeMillis());//执行任务时间-当前时间
                        }
                    }
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
        thread.start();
    }
}
public class Test_Timer2 {
    public static void main(String[] args) {
        MyTimer myTimer=new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("chenle");
            }
        },2000);

        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("zyf");
            }
        },3000);

        System.out.println("开始");
    }
}

如果你不在乎别人的态度,那么你的幸福就会变得无比简单。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1556800.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

方案研发公司服务的特点

一、服务特点&#xff1a; 1、有协助厂商在产品开发上解决问题的实践经验。 2、与国内半导体商合作&#xff0c;专营应用消费性IC&#xff0c;在供货上能以较有效率方式出货&#xff0c; 配合客户之需求。 3、长期从事专业的设计工作&#xff0c;能以较有效率方式、较专业的…

安达发|APS计划排产系统帮助纺织业实现企业数字化管理

APS&#xff08;高级计划排产系统&#xff09;是一种基于供应链管理和约束理论的计划排产工具&#xff0c;它通过模拟和优化企业的生产、物流等运作过程&#xff0c;帮助企业实现精细化管理。在纺织业中&#xff0c;APS的应用可以极大地推动企业数字化管理的进程&#xff0c;具…

【ROS 笔记1】Topic message通俗理解

前言: topic 能够将所有的独立的模块, 进行有序的交流,链接。 可以想象, roscore, 假设是一个铁路系统的总的开关,当打开总的开关(run roscore), 铁路路就可以畅通起来, 铁路畅通后, 如何进行北京站(机器人recognition)与上海站(机器人抓取)的交流。 那么我们可以从…

love 2d Lua 俄罗斯方块超详细教程

源码已经更新在CSDN的码库里&#xff1a; git clone https://gitcode.com/funsion/love2d-game.git 一直在找Lua 能快速便捷实现图形界面的软件&#xff0c;找了一堆&#xff0c;终于发现love2d是小而美的原生lua图形界面实现的方式。 并参考相关教程做了一个更详细的&#x…

第十四章 MySQL

一、MySQL 1.1 MySql 体系结构 MySQL 架构总共四层&#xff0c;在上图中以虚线作为划分。 1. 最上层的服务并不是 MySQL 独有的&#xff0c;大多数给予网络的客户端/服务器的工具或者服务都有类似的架构。比如&#xff1a;连接处理、授权认证、安全等。 2. 第二层的架构包括…

【2024系统架构设计】案例分析- 2 系统开发基础

目录 一 基础知识 二 真题 一 基础知识 1 结构化的需求分析 结构化特点:自顶向下,逐步分解,面向数据。 三大模型:

仓库规划(plan)

明天就要考试了&#xff0c;但是我正处于一点都不想学的状态 高考前我也是这样的 逆天 代码如下&#xff1a; #include<vector> #include<cstdio> using namespace std; int n, m; struct Node{int id;vector<int> d;bool operator<(const Node &t…

算法题2两数相加

给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。你可以假设除了数字 0 之外&#xff0c;这两个数都不会以 0 开…

动态规划之方格取数

方格取数 动态规划&#xff0c;数字三角形模型 题目链接 https://www.luogu.com.cn/problem/P1004 题目描述 解法一 O ( n 4 ) O(n^4) O(n4) #include<bits/stdc.h> using namespace std; int n, i, j, l, k, x, y, s; int d[55][55], f[55][55][55][55]; int main()…

等保测评-Oracle数据库

安全计算环境 身份鉴别 a)应对登录的用户进行身份标识和鉴别&#xff0c;身份标识具有唯一性&#xff0c;身份鉴别信息具有复杂度要求并定期更换 select limit from dba_profiles where profileDEFAULTand resource_namePASSWORD_VERIFY_FUNCTION; //查看密码复杂度是否开启…

web基础07-Vue

目录 一、Vue 1.概述 2.MVC与MVVM 3.快速入门 4.Vue工程的创建 &#xff08;1&#xff09;基于vue-cli &#xff08;2&#xff09;基于Vite&#xff08;推荐&#xff09; 5.Vue3核心语法 6.setup &#xff08;1&#xff09;概述 &#xff08;2&#xff09;返回值方式…

【测试工具】JMeter接口测试的简单使用

事先声明&#xff1a;博主的JMeter是3.3版本的&#xff0c;可能和最新版本的操作有些许差别 测试前的准备工作 1、先添加一个线程组&#xff1a;右击“测试计划”&#xff0c;点击“添加”—》“Threads(Users)”—》“线程组” 2、再添加一个HTTP请求&#xff0c;右击“线程…

Redis 和 Mysql 数据库数据如何保持一致性????

1、前言 我们在实际项目中经常会使用到Redis缓存用来缓解数据库压力&#xff0c;但是当更新数据库时&#xff0c;如何保证缓存及数据库一致性&#xff0c;一般我们采用延时双删策略。 目前系统中常用的做法是一个查询接口&#xff0c;先查询Redis&#xff0c;如果不存在则查询…

esp32中vscode的开发环境

vscode中安装esp32开发环境请参考&#xff1a;CSDN 1、调出esp32的控制面板View ->Command Paletter&#xff0c;或者快捷键&#xff08;ctrshitp&#xff09; 调出esp-idf的样例工程 选择ESP-IDF所在的安装路径 选择一个样例工程&#xff0c;作为工程模板 创建的新工程如…

实现ls -l 功能,index,rindex函数的使用

index(); rindex();----------------------------------------------------------------- index第一次遇到字符c&#xff0c;rindex最后一次遇到字符c&#xff0c;返回值都是从那个位置开始往后的字符串地址 #include <stdio.h> #include <sys/types.h> #include &…

MySQL Server 8.3.0 重要变更解析

MySQL Server 8.3.0 Innovation 版本是 MySQL 8.x 系列最后一个创新版本&#xff0c;下个月即将迎来 MySQL 8.4.0 LTS 长期支持版本。 关于发版模型变更&#xff0c;在之前的文章 重磅&#xff01;MySQL 8.1.0 已来&#xff01; 中已有所介绍。 这里补充一点&#xff0c;对于 M…

11-设计模式:Go常用设计模式概述

设计模式是啥呢&#xff1f;简单来说&#xff0c;就是将软件开发中需要重复性解决的编码场景&#xff0c;按最佳实践的方式抽象成一个模型&#xff0c;模型描述的解决方法就是设计模式。使用设计模式&#xff0c;可以使代码更易于理解&#xff0c;保证代码的重用性和可靠性。 …

P8681 [蓝桥杯 2019 省 AB] 完全二叉树的权值

题目描述 给定一棵包含 &#xfffd;N 个节点的完全二叉树&#xff0c;树上每个节点都有一个权值&#xff0c;按从上到下、从左到右的顺序依次是 &#xfffd;1,&#xfffd;2,⋯&#xfffd;&#xfffd;A1​,A2​,⋯AN​&#xff0c;如下图所示&#xff1a; 现在小明要把相同…

图论基础(python蓝桥杯)

图的基本概念 图的种类 怎么存放图呢&#xff1f; 优化 DFS 不是最快/最好的路&#xff0c;但是能找到一条连通的道路。&#xff08;判断两点之间是不是连通的&#xff09; 蓝桥3891 import os import sys sys.setrecursionlimit(100000) # 请在此输入您的代码 n, m map(int,…

C语言程序编译和链接

在ANSI C的任何⼀种实现中&#xff0c;存在两个不同的环境。 第1种是翻译环境&#xff0c;在这个环境中源代码被转换为可执⾏的机器指令&#xff08;⼆进制指令&#xff09;。 第2种是执⾏环境&#xff0c;它⽤于实际执⾏代码。 如果再把编译器展开成3个过程&#xff0c;那就变…