锋盈数科-知识库 Logo
首页
软件开发
计算机基础
Hello Halo
新手必读
关于本知识库
登录 →
锋盈数科-知识库 Logo
首页 软件开发 计算机基础 Hello Halo 新手必读 关于本知识库
登录
  1. 首页
  2. 软件开发
  3. 多线程案例-实现定时器

多线程案例-实现定时器

0
  • 软件开发
  • 发布于 2024-09-19
  • 15 次阅读
黄健
黄健


1.定时器是什么

定时器是软件开发中的一个重要组件,功能是当达到一个特定的时间后,就执行某个指定好的代码

定时器是一个非常常用的组件,特别是在网络编程中,当出现了"连接不上,卡了"的情况,就使用定时器做一些操作来止损

标准库中也提供了定时器

标准库中的Timer类

标准库提供了一个Timer类,Timer类的核心方法为schedule(安排,预定;将……列入计划表或清单)

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

下面是用一下定时器

可以看到有两个参数

TimerTask类就是一个实现了Runnable接口的类,来描述指定的任务

delay是指定的时间后执行任务!

运行程序,经过指定的时间后,执行了run()中的语句

2.实现定时器

定时器的核心

注册任务后需要保证任务在指定的时间要被执行
单独在定时器内部,创建一个线程,让这个线程周期性的扫描,判定任务是否到时间了,如果到时间了就执行,没到就继续等待
一个定时器能连续注册N个任务,N个任务是按照最初约定的时间按顺序执行
这N个任务肯定需要一种数据结构来保存,不难发现,我们可以使用优先级队列,我们每个任务都是带有时间的,按照时间小的作为优先级高的,此时队首元素一定是最先要执行的任务,这时候扫描线程也只需要扫描队首元素即可,不必扫描整个队.如果队首元素没有到执行时间,那么其它元素也不可能到达执行时间!!

简而言之,定时器的核心:

1.有一个扫描线程,判断是否到执行时间.

2.还得有一个数据结构保存被注册的任务.

此处优先级队列是在多线程环境下使用的,因此要关注线程安全问题!自己手动加锁,或者使用标准库提供的PriorityBlockingQueue,它既有优先级又符合线程安全的要求

实现代码

我们先创建一个任务类

class MyTask{
    //任务内容
    private Runnable runnable;
    //任务指定的时间(ms时间戳表示)
    private long time;
public MyTask(Runnable runnable, long time) {
this.runnable = runnable;
this.time = time;
    }
}
    //获取时间
    public long getTime() {
        return time;
    }
    //执行任务
    public void run(){
        runnable.run();
    }

队列里的"任务"使用Runnable表示,描述的是任务的内容

使用时间戳描述任务什么时候被执行

然后创建一个定时器

class MyTimer{
    //扫描线程
    private Thread t = null;
    //阻塞优先级队列来保存任务
    private PriorityBlockingQueue<MyTask> queue =
            new PriorityBlockingQueue<>();
}

我们要给定时器类提供一个"schedule"方法来注册任务

//指定两个参数,一个是任务内容,一个是多长时间后执行任务
    public void schedule(Runnable runnable,long after){
        //注意时间的换算
        MyTask myTask = new MyTask(runnable,System.currentTimeMillis()+after);
        queue.put(myTask);
    }

接下来要实现一个比较麻烦的操作,就是扫描线程的实现

public MyTimer(){
        t = new Thread(()->{
           while (true){
               try {
                   //取出队首元素,检查是否到执行时间了
                   //如果到了,就执行
                   //如果没到,就放回队列
                   //如果没有元素证明没有任务,会阻塞等待
                   MyTask myTask = queue.take();
                   long curTime = System.currentTimeMillis();
                   if(curTime < myTask.getTime()){
                       //没到点,不用执行
                       //到点了,开始执行
                       queue.put(myTask);
                   }else{
                       myTask.run();
                   }
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
        });
    t.start();
    }

上述代码大致实现了扫描线程的功能,但是还存在两个问题

第一个问题,我们还要明确我们的任务优先级是怎样的,还没指定

此时我们如果测试:

public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1");
            }
        },1000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2");
            }
        },2000);
    }


因为两个任务的优先级关系还没用comparable来设置,或者单独实现一个比较器comparator

运行程序

第二个问题,如果队首任务是四点进行执行,在两点的时候,线程开始扫描,就会一直从队首取出检查,发现没到执行时间,又放回去,反反复复!!直到四点开始执行.我们使用优先级队列来存储的,放回元素,堆就会进行一次调整,将这个任务又调整至队首,下次取出,还是这个元素!

这个循环没有阻塞,会快速的进行循环,是没有意义的,占用了cpu资源.这种情况称为"忙等”,我们要对代码进行调整,进行阻塞式等待,sleep,wait..

如果等待时间明确,我们使用sleep可行吗?

此处看似等待时间是明确的,但是我们可能任意时间会来一个新的任务调用schedule,注册任务,那么队首元素就换了,必须得扫描出来.

如果用sleep,那么在sleep过程中就可能注册新的任务,如果在队首元素执行的时间前就要执行新注册的任务,然而用的sleep,就会错过这个任务的执行了
因此使用wait()notify()更合适,使用wait()进行等待,如果有新任务调用schedule,就notify(),重新检查一次,计算等待的时间
并且,wait()还有个超时时间的版本,如果没有新任务,则最多等到队首元素的执行时间就自动唤醒了

这样改动之后,我们既不会一直重复无用操作,也不会错过执行新注册任务

线程安全问题

代码到这里,还有个线程安全问题

我们考虑一个极端情况

如果代码执行到wait之前,这个线程被调度走了,当线程又被调度执行时,接下来就要进行wait操作,它的wait时间是算好了的,比如curTime是13:00,getTime是14:00,即将会wait一个小时,但是还没执行wait.

在该线程被调度走的过程中,如果另一个线程调用了schedule,注册了一个13:30执行的任务,此时schedule会执行notify()将wait()唤醒,但是扫描线程的wait()还没有执行呢,所以notify并没有实际作用,虽然新任务插入到队列中了,也是在队首.但是这个线程紧接又执行wait()一个小时,错过了这次任务的执行时间13:30

这都是多线程随机调度产生的,take和wait操作并非是原子的,如果这个过程是原子的,给它加上锁,保证不会有新的任务过来,就解决问题了,换言之就是要保证每次notify时,确实都在wait!

我们将锁的粒度变大,保证take和wait操作是原子的,就不会出现线程安全问题了



原文链接: https://blog.csdn.net/chenchenchencl/article/details/128625021

标签: #软件开发 1171 #定时器 1
相关文章

万字:支付“核心系统”详解 2024-11-02 15:33

专栏作者:隐墨星辰 \| 主编:陈天宇宙 这篇文章也尝试化繁为简,探寻支付系统的本质,讲清楚在线支付系统最核心的一些概念和设计理念。 虽然支付行业已经过了风头最劲的时光,但跨境支付仍然在蓬勃发展,每年依然有很多新人进入这个行业,这篇文章尝试为这些刚入行的新人提供一点帮助。 文章只介绍一些支付行业十几

资深支付架构师视角:实战从问题定义到代码落地的完整套路 2024-11-02 15:33

前言 今天从一个实际案例入手,介绍站在架构师的角度,如何识别并定义问题,提炼需求,技术方案选型,再到详细设计,最后利用AI的能力协助写出核心的代码,验证与调优。 解决问题存在一定的模式,也可以称之为框架,总结出自己的思考和解题框架,以后再碰到同类型的问题就可以如庖丁解牛一样容易。 很多年前,我写代码

Spring 实现 3 种异步接口 2024-10-18 09:07

大家好,我是苏三~ 如何处理比较耗时的接口? 这题我熟,直接上异步接口,使用 Callable、WebAsyncTask 和 DeferredResult、CompletableFuture等均可实现。 但这些方法有局限性,处理结果仅返回单个值。在某些场景下,如果需要接口异步处理的同时,还持续不断地

重学SpringBoot3-集成Redis(五)之布隆过滤器 2024-10-08 11:24

更多SpringBoot3内容请关注我的专栏:《SpringBoot3》 期待您的点赞👍收藏⭐评论✍ 重学SpringBoot3-集成Redis(五)之布隆过滤器 1. 什么是布隆过滤器? * 基本概念 适用场景 2. 使用 Redis 实现布隆过滤器 * 项目依赖 Redis 配置

设计模式第16讲——迭代器模式(Iterator) 2024-10-08 11:24

一、什么是迭代器模式 迭代器模式是一种行为型设计模式,它提供了一种统一的方式来访问集合对象中的元素,而不是暴露集合内部的表示方式。简单地说,就是将遍历集合的责任封装到一个单独的对象中,我们可以按照特定的方式访问集合中的元素。 二、角色组成 抽象迭代器(Iterator):定义了遍历聚合对象所需的方法

vue2路由和vue3路由区别及原理 2024-10-08 11:24

一、Vue2 与 Vue3 路由的区别 1. 创建路由实例方式的不同 Vue 2 中,通过 Vue.use() 注册路由插件,并通过 new VueRouter() 来创建路由实例。 import Vue from 'vue';import VueRouter from 'vue-router';i

目录

IT 外包服务商

  • 意见投递
  • zyf6619

软件开发应用

主菜单

  • 首页
  • 软件开发
  • 计算机基础
  • Hello Halo
  • 新手必读
  • 关于本知识库
Copyright © 2024 your company All Rights Reserved. Powered by Halo.