SpringBoot定时任务里的多线程

news2024/11/13 11:02:54

SpringBoot定时任务里的多线程

  • 提示
  • 前言
  • 遇到的问题
  • 验证与解决
    • 验证
      • 单线程执行单任务
        • 分析
        • 代码及结果
      • 单线程执行多任务
    • 解决
      • 实现单任务的多线程
        • 为每个任务创建一个子线程
      • 解决多任务的多线程
        • 设定固定容量线程池
        • 动态设定容量线程池
        • 固定线程池和动态线程池的选择
  • 简单总结
  • 借鉴及引用

提示

本文并不涉及cron表达式,而是关注于执行定时任务的线程

前言

我们在搭建系统的时候,不可避免地要用到定时任务,注解为 @Scheduled(cron = "cron表达式"),就像闹钟那样,每天定时执行叫醒你起来上早八或者上班的任务。

在这里插入图片描述

遇到的问题

在使用过程中,遇到了不少问题。

  1. 在定时任务中默认使用单线程的方式来执行定时任务。

在这里插入图片描述
也就是任务1、2、3之中只能有任一一个在执行,而无法同时进行。

比如在任务1任务2都是从0秒开始运行,那么在0秒的时候本来预期是两者一起运行,也就是以下这样:在这里插入图片描述

但由于默认是单线程,所以它就可能变成了以下两种情况:
先执行任务1在执行任务2
在这里插入图片描述
或者相反
在这里插入图片描述
同时执行的任务,在单线程的情况下随机执行一个

所以你可以看到单线程对于多个任务而言,有很多不方便的地方。比如某个任务耗费时间比较长,那么其他的定时任务就必须要等它执行结束才能继续执行。又或者有的任务直接挂掉了,导致所有的定时任务都无法执行。

  1. 多线程的定时任务中,每个任务只分配到一个线程

在上面的讨论中,我们注意到单线程的弊端,那么自然有解决办法。那就是多线程执行定时任务,多线程执行任务就是以下这样:
在这里插入图片描述
但大家有没有发现一个问题,就是单个线程能应付得了单个任务吗?

举个简单的例子:我们上面的提到过的耗费时间比较长的问题。

比如我们任务10秒开始每5秒执行一次,但是其任务执行时间要7秒。那么就可能导致什么问题?大家可以想一下。
3

2

1

你觉得是这样?
在这里插入图片描述

但其实是这样:

在这里插入图片描述

单线程应付不了单个任务的时候,就可能导致我们的任务并不会按照我们的预期去执行。

如上面那样由于上一次任务的执行时间大于我的定时间隔时间,导致我漏掉下一次任务,又或者某次执行挂掉卡住了导致后续定时任务无法执行

怎么解决上述的问题?
其实这个问题,可以具体化到如何让任务的每次执行互不影响
3

2

1

答案揭晓:多线程

任务的每次执行分配一个线程,使得每次任务的执行两两独立,互不影响。也就是一下这样:

在这里插入图片描述

验证与解决

下面我们就来在项目中验证上述中提到的相关问题,请注意下面的验证顺序与上面提到的问题顺序的略有不同

验证

以下内容仅仅验证上面的叙述在SpringBoot环境中的定时任务是否准确。

单线程执行单任务

当任务的执行时间,大于你设定的时间时,单线程、单个定时任务如何执行?

以上面提到过的任务一的执行举例:从0秒开始每5秒钟执行一次,但是你的任务每次执行时间需要7秒,请设想一下在一分钟内它最终执行多少次?

分析

第一次执行完是7秒,那么此时已经过了5秒的那个点,所以下一次到10秒才会执行。也就是对应秒数到达0 ,10 ,20,30,40,50的时候才会执行,总共执行6次。也就是上图中的
在这里插入图片描述

代码及结果

来看以下定时任务代码:

//从0开始 每五秒执行一次
@Scheduled(cron ="0/5 * * * * *")
    public void testTread4() throws InterruptedException {
        //在一分钟内执行
        if(DateUtil.formatStr(new Date()).compareTo("2023-05-16 17:06") >= 0
        && DateUtil.formatStr(new Date()).compareTo("2023-05-16 17:07") < 0) {
            //睡眠7秒钟
            Thread.sleep(7000);
            //执行结束时间
            System.out.println("执行任务" + DateUtil.formatStr(new Date()));
        }
    }

执行结果(注意:这里的输出时间为定时任务执行结束时间而非开始时间):
在这里插入图片描述
很明显符合我们预期的结果,也就是单线程情况下,如果在执行任务期间还要执行新的任务,那么该新任务就会被忽略

单线程执行多任务

单线程执行多任务情况下,如果某个任务执行过久,由于它是单线程,所以会导致后续任务也被要等待它执行结束,才能继续执行。也就是上图中的

在这里插入图片描述
或者
在这里插入图片描述

来看以下这段代码的逻辑

//一秒执行一次
    @Scheduled(cron ="0/1 * * * * *")
    public void testTread2() {
        System.out.println("定时任务1 开始  "+ DateUtil.formatStr(new Date()));
        //假设以下是它需要执行的任务内容 以下代码执行时间超过一秒
        for(int i =0; i < 1000000 * 500;i++) {
            String str = "diuhfiahiudhfadfjkakhfkjahsjcndjakdjfoiejalkdjfialdjcxkjhaoifkdhjafhdkahjfkdahfaldkjfianvnjvhdhuiehrkahkjdfakhuidhfakjhducian";
            String result1 = "";
            result1 = result1 + str.indexOf(i % 125);
        }
        System.out.println("定时任务1 结束  "+ DateUtil.formatStr(new Date()));

    }

    //一秒执行一次
    @Scheduled(cron ="0/1 * * * * *")
    public void testTread3() {
        System.out.println("定时任务2 开始  "+ DateUtil.formatStr(new Date()));

        //假设以下是它需要执行的任务内容 常规时间执行是两秒左右
        for(int i =0; i < 1000000 * 50;i++) {
            String str = "diuhfiahiudhfadfjkakhfkjahsjcndjakdjfoiejalkdjfialdjcxkjhaoifkdhjafhdkahjfkdahfaldkjfianvnjvhdhuiehrkahkjdfakhuidhfakjhducian";
            String result1 = "";
            result1 = result1 + str.indexOf(i % 125);
        }
        System.out.println("定时任务2 结束  "+ DateUtil.formatStr(new Date()));

    }

我们来看下结果:
在这里插入图片描述
可以明显看到,当有任务1未执行完时,即使任务2到达执行的时间,也没有执行

那么,如何让单线程定时任务变成多线程呢?

画外音:忙碌了一整天的鱼师傅回到家中,发现了这个问题,最复杂的问题往往只需要最朴素的解决办法,他打开了csdn
在这里插入图片描述
在这里插入图片描述
不好意思,串台了。

解决

这里需要分为两个方面:

  1. 单个任务,多个线程:每次的执行都交给一个新的线程
    对应我们上面提到的:
    在这里插入图片描述
  2. 多个定时任务对应多个线程:让每个任务单独分到一个线程
    也就是上面的这个图
    在这里插入图片描述

简单来讲,单线程多任务的情况就是,一个定时任务需要多个线程来执行。

多任务多线程就是,个定时任务需要多个线程来执行。

实现单任务的多线程

任务的每次执行交给一个新的线程,任务的每次执行独立

为每个任务创建一个子线程

每个任务里面的执行内容新建一个子线程

修改上述代码:

@Scheduled(cron ="0/5 * * * * *")
    public void testTread4() {
        //为每一个子任务创建一个新的线程执行 也可以将具体的任务内容封装到一个继承Thread或者实现Runnable接口的类中
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //在一分钟内执行
                    if(DateUtil.formatStr(new Date()).compareTo("2023-05-16 17:25") >= 0
                            && DateUtil.formatStr(new Date()).compareTo("2023-05-16 17:26") < 0) {
                        //睡眠7秒钟
                        Thread.sleep(7000);
                        System.out.println("执行任务" + DateUtil.formatStr(new Date()));
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

执行结果

//0秒开始执行 执行7秒 07结束
执行任务2023-05-16 17:25:07
//5秒开始执行 12结束
执行任务2023-05-16 17:25:12
//10秒开始执行 17结束
执行任务2023-05-16 17:25:17
执行任务2023-05-16 17:25:22
执行任务2023-05-16 17:25:27
执行任务2023-05-16 17:25:32
执行任务2023-05-16 17:25:37
执行任务2023-05-16 17:25:42
执行任务2023-05-16 17:25:47
执行任务2023-05-16 17:25:52
执行任务2023-05-16 17:25:57
//25:55秒开始执行 26:02结束
执行任务2023-05-16 17:26:02

可以看到执行了12次,并且每个任务的开始时间是00 - 05 - 10 - 15 - .....-55,互不影响,两两独立。

解决多任务的多线程

这个逻辑只需要让每个定时任务各自拥有一个线程,每个定时任务之间独立,你干你的,我干我的,互不影响。

设定固定容量线程池

一种方法是添加配置定时任务的配置类,修改默认的线程池容量(默认为1,也就是单线程)

新建ScheduleConfig.java配置文件,内容如下:

import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Component;

@Component
public class ScheduleConfig {

    @Bean
    public TaskScheduler taskScheduler() {
        //定时任务线程池
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        // 设置线程池大小为10
        taskScheduler.setPoolSize(10);
        return taskScheduler;
    }
}

执行结果:
在这里插入图片描述
可以看到任务1和任务2同时开始执行,不再出现谁等谁的情况了。

动态设定容量线程池

另一种方法是实现任务调度配置接口SchedulingConfigurer ,根据任务数量配置线程池钟线程最大数量,也可以达到上面的效果。

@Component
@Configuration      
@EnableScheduling
public class MySchedule
		//需要实现这个接口
        implements SchedulingConfigurer {
    //配置定时任务线程池信息 有多少定时任务就配置多大的线程池 任务中新建的子线程并不由是此线程池里的对象
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
    //获取所有方法
        Method[] methods = MySchedule.class.getMethods();
        //线程池默认大小10
        int defaultPoolSize = 10;
        //实际大小为0
        int corePoolSize = 0;
        Assert.notNull(methods , "并无定时任务");
        if (methods.length > 0) {
        //遍历所有任务 统计所有带定时任务注解的方法个数
            for (Method method : methods) {
                Scheduled annotation = method.getAnnotation(Scheduled.class);
                if (annotation != null) {
                    corePoolSize++;
                }
            }
            //最小为10
            if (defaultPoolSize > corePoolSize)
                corePoolSize = defaultPoolSize;
        }

        System.out.println("线程池大小:" + corePoolSize);
        taskRegistrar.setScheduler(Executors.newScheduledThreadPool(corePoolSize));
    }

 //一秒执行一次
    @Scheduled(cron ="0/1 * * * * *")
    public void testTread2() {
        System.out.println("定时任务1 开始  "+ DateUtil.formatStr(new Date() , DateUtil.MillisecondPattern));
        //假设以下是它需要执行的任务内容 以下代码执行时间超过一秒
        for(int i =0; i < 1000000 * 500;i++) {
            String str = "diuhfiahiudhfadfjkakhfkjahsjcndjakdjfoiejalkdjfialdjcxkjhaoifkdhjafhdkahjfkdahfaldkjfianvnjvhdhuiehrkahkjdfakhuidhfakjhducian";
            String result1 = "";
            result1 = result1 + str.indexOf(i % 125);
        }
        System.out.println("定时任务1 结束  "+ DateUtil.formatStr(new Date() , DateUtil.MillisecondPattern));

    }

    //一秒执行一次
    @Scheduled(cron ="0/1 * * * * *")
    public void testTread3() {
        System.out.println("定时任务2 开始  "+ DateUtil.formatStr(new Date() , DateUtil.MillisecondPattern));

        //假设以下是它需要执行的任务内容 常规时间执行是两秒左右
        for(int i =0; i < 1000000 * 50;i++) {
            String str = "diuhfiahiudhfadfjkakhfkjahsjcndjakdjfoiejalkdjfialdjcxkjhaoifkdhjafhdkahjfkdahfaldkjfianvnjvhdhuiehrkahkjdfakhuidhfakjhducian";
            String result1 = "";
            result1 = result1 + str.indexOf(i % 125);
        }
        System.out.println("定时任务2 结束  "+ DateUtil.formatStr(new Date() , DateUtil.MillisecondPattern));

    }
}

执行结果
在这里插入图片描述
可以看到两个任务你干你的 我干我的,不再出现,我要等你干完,我才能干的情况。

需要注意的是,这里设定的池是全局通用的,任何定时任务都可以用这个池,而且共用一个。如果存在多个执行定时任务的类,并且每个类中都有上述的构建池的操作,那么会以最后执行的设置池的容量为准。(这个大家可以自己测试一下,输出的时候加上Thread.currentThread().getName()这个 – 获取线程名称,用于区分线程池里的线程即可。)

比如我有两个定时任务类,第一个里面有2个定时任务,第二个里面有18个定时任务:
在这里插入图片描述
那么执行的时候MySchedule设置的池大小为10West01设置的池大小为18。最后各个任务执行的时候,所用到的池大小为18

如果将West01更改为Aest01,那么最后执行用到的池就是MySchedule里面设置的池容量,大小为10

因为按照名称排序并且加载MySchedule的加载排在Aest01之后,West01之前,容量以最后的设置为准。

MyScheduleWest01
在这里插入图片描述MyScheduleAest01
在这里插入图片描述

固定线程池和动态线程池的选择

如果你所有的定时任务都在一个类里面,毫无疑问第二种动态线程池就是最佳选择。

但是你有多个定时任务类,选择第一种完全是ok的,但计算估计有点麻烦需要计算一下,设置为多少合理。

简单总结

在文中,我们从简单的原理描述,到验证,再到最后实操
从默认的单线程执行任务多线程执行单任务,再到多线程执行多任务

想必你会觉得“多线程这么爽那还用什么单线程?”但其实多线程的问题也不少,简单对比一下:

  1. 单线程占用资源,多线程占用资源。在多线程情况下,如果某些任务执行时间过长,而新的任务又在不断地产生新的进程,将会迅速耗尽计算机资源
  2. 单线程不需要考虑资源竞争,多线程需要对共享资源做对应的处理。这个比较容易理解。比如买票,有两个售票员卖票,现在只剩下一张票,这时他们同时接到顾客,都告诉顾客有票,但实际操作的时候手速快的那个把票拿走了,那么后者就没有票可卖了。
  3. 单线程日志记录简单、清楚,多线程日志记录混乱。前者是按顺序说话,后者是这里来一句,那里来一句。

就说到这个里了,还是那句话,没有最好的解决方法,只有最合适的解决方法

借鉴及引用

Java 多线程编程基础(详细)
Springboot @Scheduled定时任务单线程执行问题

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

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

相关文章

基于卷积的图像分类识别(三):VGGNet

系列文章目录 本专栏介绍基于深度学习进行图像识别的经典和前沿模型&#xff0c;将持续更新&#xff0c;包括不仅限于&#xff1a;AlexNet&#xff0c; ZFNet&#xff0c;VGG&#xff0c;GoogLeNet&#xff0c;ResNet&#xff0c;DenseNet&#xff0c;SENet&#xff0c;Mobile…

分布式锁解决方案_Redis实现的分布式锁原理

获取锁 互斥&#xff1a;确保只有一个线程获得锁 添加锁 利用setnx的互斥性 启动Redis的我们进行测试看看怎么能某个数据来获得锁 setnx命令表示上锁只要是aa被创建出来我们不删除aa或者设定时间自动删除&#xff0c;那么这把锁就不能被释放开 释放锁 手动释放锁 如果我们…

Ubuntu20.04安装VTK8.2

Ubuntu20.04安装VTK8.2 相关依赖的安装安装步骤参考文献相关依赖的安装 sudo apt install checkinstall sudo apt-get install cmake-curses-gui sudo apt-get install freeglut3-dev sudo apt install libqt5x11extras5-dev sudo apt install cmake-qt-gui安装步骤 1、从官网…

【运筹优化】元启发式算法详解:禁忌搜索算法(Tabu Search,TS)+ 案例讲解代码实战

文章目录 一、介绍二、The Classical Vehicle Routing Problem 经典的车辆路径问题三、基本概念3.1 历史背景3.2 禁忌搜索3.3 搜索空间和邻域结构3.4 Tabus 禁忌3.5 解禁准则3.6 简单禁忌搜索的模板3.7 终止标准3.8 概率TS和候选列表 四、中级概念4.1 搜索强化4.2 多样化4.3 允…

Maven基础使用

Maven 学习目标 理解Maven的用途掌握Maven的基本操作掌握Maven如何创建Web项目 Maven是什么 面临问题 在学习Maven之前&#xff0c;我们先来看一下我们现在做的项目都有哪些问题。假设你现在做了一个crm的系统&#xff0c;项目中肯定要用到一些jar包&#xff0c;比如说myb…

opencv_c++学习(九)

一、图像二值化 固定阈值二值化 threshold ( lnputArray src, outputArray dst,double thresh, double maxvall, int typesrc:待二值化的图像&#xff0c;图像只能是CV_8U和CV_32F两种数据类型。对于图像通道数目的要求和选择的二值化方法相关。 dst:二值化后的图像&#xff…

【数据结构】哈希表上——开放寻址法

文章目录 前言映射哈希冲突开放寻址法思路分析结构分析函数实现插入删除寻找 结语 前言 大家好久不见&#xff0c;今天来讲解一下哈希表的基本原理并使用开放寻址法实现简单哈希表。 映射 哈希表的实现思路就是将一组数据映射成另外一组可以直接查找的数据&#xff0c;假如有…

【LeetCode】每日一题:链表部分经典题型

文章目录 1.反转链表2.链表的中间节点3.合并两个有序链表4.相交链表5.环形链表6.环形链表Ⅱ ​&#x1f47b;内容专栏&#xff1a;《LeetCode刷题专栏》 &#x1f428;本文概括&#xff1a;归纳链表部分经典题型。206.反转链表、876.链表的中间节点、21.合并两个有序链表、160.…

如何优雅地彻底解决 antd 全局样式问题

背景 由于某些原因&#xff0c;我们团队负责在组件 上做二次开发&#xff0c;简单理解就是封装组件&#xff0c;组件库选择了 antd&#xff0c;尴尬的是引入之后发现&#xff0c;父组件 自身是带一套全局样式的&#xff0c;而 子组件antd 又带了一套全局样式&#xff0c;导致 子…

字节原来这么容易进,是面试官放水,还是公司实在是太缺人?

本人211非科班&#xff0c;之前在字节和腾讯实习过&#xff0c;这次其实没抱着什么特别大的希望投递&#xff0c;没想到字节可以再给我一次机会&#xff0c;还是挺开心的。 本来以为有个机会就不错啦&#xff01;没想到能成功上岸&#xff0c;在这里要特别感谢帮我内推的同学&…

《程序员面试金典(第6版)》面试题 16.24. 数对和

题目描述 设计一个算法&#xff0c;找出数组中两数之和为指定值的所有整数对。一个数只能属于一个数对。 题目传送门 示例 1: 输入: nums [5,6,5], target 11 输出: [[5,6]]示例 2: 输入: nums [5,6,5,6], target 11 输出: [[5,6],[5,6]]提示&#xff1a; nums.length &…

基于物联感知和GNSS技术的铁塔安全监测解决方案

监测背景 电力铁塔是承载电力供应的重要设施&#xff0c;它的安全性需要得到可靠的保障。但是铁塔一般安装在户外&#xff0c;分布广泛&#xff0c;且有很多安装在偏远地区&#xff0c;容易受到自然、人力的影响和破环。因此需要使用辅助的方法实时监控通信塔的安全状态&#x…

5th-Generation Mobile Communication Technology(二)

目录 一、5G/NR 1、 快速参考&#xff08;Quick Reference&#xff09; 2、5G Success 3、5G Challenges 4、Qualcomm Videos 二、PHY and Protocol 1、Frame Structure 2、Numerology 3、Waveform 4、Frequency Band 5、BWP 6、Synchronization 7、Beam Management 8、CSI Fra…

一文4000字从0到1用WebDriver+Selenium实现浏览器自动化

前言 Selenium是一款可以自动化操作浏览器的开源项目&#xff0c;最初的目的是浏览器功能的自动化测试&#xff0c;但是随着项目的发展&#xff0c;人们根据它的特性也用来做一些更多的有意思的功能而不仅仅是UI的自动化测试工具。就像Selenium官方网站上描述的那样&#xff0…

chatgpt怎么搭建,以及怎么接入企业微信工作台

gpt目前也用了一段时间了&#xff0c;用起来只能说越来越顺手&#xff0c;然后集成到企业微信让公司全部成员都用起来了。 使用界面如下&#xff1a; 主界面 功能&#xff1a; 1、通过企业微信认证后访问使用&#xff0c;防止非公司人员入侵 2、记录用户姓名和提问内容&#x…

IOS证书制作教程

转载&#xff1a;IOS证书制作教程 点击苹果证书 按钮 点击新增 输入证书密码&#xff0c;名称 这个密码不是账号密码&#xff0c;而是一个保护证书的密码&#xff0c;是p12文件的密码&#xff0c;此密码设置后没有其他地方可以找到&#xff0c;忘记了只能删除证书重新制作&am…

05-函数

函数的定义 函数名 函数名的后面有个圆换号()&#xff0c;代表这个为函数&#xff0c;不是普通的变量名。 形参 在定义函数时指定的形参&#xff0c;在未出现函数调用时&#xff0c;它们并不占内存中的存储单元&#xff0c;因此称它们是形式参数或虚拟参数&#xff0c;简称…

.Net6 使用aspose.cells23.5.0

一、测试代码 internal class Program { static void Main(string[] args) { WorkbookDesigner wb new WorkbookDesigner(new Workbook()); var style new CellsFactory().CreateStyle(); style.Borders.SetColor(C…

LED显示屏控制系统分类

LED显示屏的控制系统可以根据不同的特点和功能进行分类。以下是常见的LED显示屏控制系统分类&#xff1a; 同步控制系统&#xff1a;同步控制系统通过传输同步信号来控制LED显示屏&#xff0c;确保多个显示屏之间的内容同步显示。同步控制系统适用于大型LED显示屏&#xff0c;如…

通过chatGPT学习:kubernetes中的list-watch机制介绍

1、 请解释一下&#xff0c;在kubernetes中的list-watch机制&#xff1f; Kubernetes是一个开源的容器编排和管理系统&#xff0c;它可以有效地管理大规模的容器化应用程序。 在Kubernetes中&#xff0c;list-watch机制是一种重要的机制&#xff0c;用于监视资源的变化并及时…