@EnableScheduling 和 @Scheduled 实现定时任务的任务延期问题

前言

在复盘 ieg 一面看到定时任务阻塞的问题时,研究了下 @EnableScheduling 的源码,觉得可以单开一篇文章讲一讲

本文主要讲述了使用 @EnableScheduling 可能出现的线程阻塞导致定时任务延期的问题,也顺便解释了动态定时任务源码上的实现

引用文章:

@Schedule定时任务+分布式环境:@Schedule定时任务+分布式环境,这些坑你一定得注意!!! (qq.com)

java 中的线程池参数:java中四种线程池及poolSize、corePoolSize、maximumPoolSize_maximum-pool-size-CSDN博客

线程池的拒绝策略:线程池的RejectedExecutionHandler(拒绝策略)-CSDN博客

Java 中实现定时任务:Java中实现定时任务,有多少种解决方案?好久没更新博客了,最近上班做了点小东西,总结复盘一下。主要介绍了定时任务的三种 - 掘金 (juejin.cn)

线程阻塞问题

问题根源

Java中 使用 Springboot 自带的定时任务 @EnableScheduling 和 @Scheduled 注解,会装配一个 SchedulingConfiguration 的类

@Target(ElementType.TYPE)  
@Retention(RetentionPolicy.RUNTIME)  
@Import(SchedulingConfiguration.class)  
@Documented  
public @interface EnableScheduling {  
  
}
@Configuration(proxyBeanMethods = false)  
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)  
public class SchedulingConfiguration {  
    @Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)  
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)  
    public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {  
        return new ScheduledAnnotationBeanPostProcessor();  
    }  
}

这个配置类又会创建一个 ScheduledAnnotationBeanPostProcessor 的 Bean

在这个类的无参构造中又初始化了一个 ScheduledTaskRegistrar 的对象

public ScheduledAnnotationBeanPostProcessor() {  
    this.registrar = new ScheduledTaskRegistrar();  
}

在 创建单例或刷新上下文之后,会执行 finishRegistration 方法,最后执行 registrar 的 afterPropertiesSet 方法:

@Override  
public void afterSingletonsInstantiated() {  
    // Remove resolved singleton classes from cache  
    this.nonAnnotatedClasses.clear();  

    if (this.applicationContext == null) {  
        // Not running in an ApplicationContext -> register tasks early...  
        finishRegistration();  
    }  
}  
  
@Override  
public void onApplicationEvent(ContextRefreshedEvent event) {  
    if (event.getApplicationContext() == this.applicationContext) {  
        // Running in an ApplicationContext -> register tasks this late...  
        // giving other ContextRefreshedEvent listeners a chance to perform  
        // their work at the same time (e.g. Spring Batch's job registration).  
        finishRegistration();  
    }  
}  
  
private void finishRegistration() {  
    if (this.scheduler != null) {  
        this.registrar.setScheduler(this.scheduler);  
    }  
    
    // ...
    
    this.registrar.afterPropertiesSet();  
}

ScheduledTaskRegistrar 的成员变量包括任务的执行器以及几种类型的定时任务列表

@Nullable  
private TaskScheduler taskScheduler;  
  
@Nullable  
private ScheduledExecutorService localExecutor;  
  
@Nullable  
private List<TriggerTask> triggerTasks;  
  
@Nullable  
private List<CronTask> cronTasks;

afterPropertiesSet 方法会获取一个执行器

@Override  
public void afterPropertiesSet() {  
    scheduleTasks();  
}  
  
/**  
* Schedule all registered tasks against the underlying  
* {@linkplain #setTaskScheduler(TaskScheduler) task scheduler}.  
*/  
@SuppressWarnings("deprecation")  
protected void scheduleTasks() {  
    if (this.taskScheduler == null) {  
        this.localExecutor = Executors.newSingleThreadScheduledExecutor();  
        this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);  
    }  
    if (this.triggerTasks != null) {  
        for (TriggerTask task : this.triggerTasks) {  
            addScheduledTask(scheduleTriggerTask(task));  
        }  
    }  
    if (this.cronTasks != null) {  
        for (CronTask task : this.cronTasks) {  
            addScheduledTask(scheduleCronTask(task));  
        }  
    }  
    if (this.fixedRateTasks != null) {  
        for (IntervalTask task : this.fixedRateTasks) {  
            addScheduledTask(scheduleFixedRateTask(task));  
        }  
    }  
    if (this.fixedDelayTasks != null) {  
        for (IntervalTask task : this.fixedDelayTasks) {  
            addScheduledTask(scheduleFixedDelayTask(task));  
        }  
    }  
}

进入 newSingleThreadScheduledExecutor 可以看到,默认使用了一个 corePoolSize 为 1, maximumPoolSize 为 Integer.MAX_VALUE 的线程池

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {  
    return new DelegatedScheduledExecutorService  
        (new ScheduledThreadPoolExecutor(1));  
}
public ScheduledThreadPoolExecutor(int corePoolSize) {  
    super(corePoolSize, Integer.MAX_VALUE,  
        DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,  
        new DelayedWorkQueue());  
}
public ThreadPoolExecutor(int corePoolSize,  
                        int maximumPoolSize,  
                        long keepAliveTime,  
                        TimeUnit unit,  
                        BlockingQueue<Runnable> workQueue) {  
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,  
    Executors.defaultThreadFactory(), defaultHandler);  
}

而线程池主要有几个重要的参数分别是:

  1. corePoolSize:线程池的基本大小。
  2. maximumPoolSize:线程池中允许的最大线程数。
  3. poolSize:线程池中当前线程的数量。

当提交一个新任务时,若

  1. poolSize < corePoolSize : 创建新线程处理该任务
  2. poolSize = corePoolSize : 将任务置于阻塞队列中
  3. 阻塞队列的容量达到上限,且这时 poolSize < maximumPoolSize :
  4. 阻塞队列满了,且 poolSize = maximumPoolSize : 那么线程池已经达到极限,会根据饱和策略 RejectedExecutionHandler 拒绝新的任务,默认是 AbortPolicy 会丢掉任务并抛出异常

解决方案

注入自己编写的线程池,自行设置参数:

@Configuration  
 public class MyTheadPoolConfig {  
   
     @Bean  
     public TaskExecutor taskExecutor() {  
         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
         //设置核心线程数  
         executor.setCorePoolSize(10);  
         //设置最大线程数  
         executor.setMaxPoolSize(20);  
         //缓冲队列200:用来缓冲执行任务的队列  
         executor.setQueueCapacity(200);  
         //线程活路时间 60 秒  
         executor.setKeepAliveSeconds(60);  
         //线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池  
         // 这里我继续沿用 scheduling 默认的线程名前缀  
         executor.setThreadNamePrefix("nzc-create-scheduling-");  
         //设置拒绝策略  
         executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());  
         executor.setWaitForTasksToCompleteOnShutdown(true);  
         return executor;  
     }  
 }

在定时任务的类上再加一个 @EnableAsync 注解,给方法添加一个 @Async 即可

@Slf4j  
@Component  
@EnableAsync  
@EnableScheduling  
public class ScheduleService {  

    @Autowired  
    TaskExecutor taskExecutor;  

    @Async(value = "taskExecutor")  
    @Scheduled(cron = "0/5 * * * * ? ")  
    public void testSchedule() {  
         try {  
             Thread.sleep(10000);  
             log.info("当前执行任务的线程号ID===>{}", Thread.currentThread().getId());  
         } catch (Exception e) {  
             e.printStackTrace();  
         }   
    }  
}

动态定时任务

上面提到了

@EnableScheduling 导入了 SchedulingConfiguration,SchedulingConfiguration 又创建了 ScheduledAnnotationBeanPostProcessor 的Bean,ScheduledAnnotationBeanPostProcessor 又实例化了 ScheduledTaskRegistrar 对象,即

@EnableScheduling -> SchedulingConfiguration -> ScheduledAnnotationBeanPostProcessor -> ScheduledTaskRegistrar

实际上,在 ScheduledAnnotationBeanPostProcessor 的 finishRegistration 方法中,会先获取所有实现了 SchedulingConfigurer 接口的 Bean,并执行他们的 configureTasks 方法


private void finishRegistration() {  
    if (this.scheduler != null) {  
        this.registrar.setScheduler(this.scheduler);  
    }  

    if (this.beanFactory instanceof ListableBeanFactory) {  
        Map<String, SchedulingConfigurer> beans =  
        ((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);  
        List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values());  
        AnnotationAwareOrderComparator.sort(configurers);  
        for (SchedulingConfigurer configurer : configurers) {  
            configurer.configureTasks(this.registrar);  
        }  
    }
    // ...
}

我们可以通过配置一个实现了 SchedulingConfigurer 接口的 Bean,实现动态加载定时任务的执行时间

@Data  
@Slf4j  
@Component  
@RequiredArgsConstructor  
@PropertySource("classpath:task-config.ini")  
public class ScheduleTask implements SchedulingConfigurer {  
  
    // private Long timer = 100 * 1000L;  

    @Value("${printTime.cron}")  
    private String cron;  

    @Override  
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {  
        // 间隔触发的任务  
        taskRegistrar.addTriggerTask(new Runnable() {  
        @Override  
        public void run(){  
           // ...
        }
        }, new Trigger() {  
        @Override  
        public Date nextExecutionTime(TriggerContext triggerContext) {  
            // 使用CronTrigger触发器,可动态修改cron表达式来操作循环规则  
            CronTrigger cronTrigger = new CronTrigger(cron);  
            Date nextExecutionTime = cronTrigger.nextExecutionTime(triggerContext);  
            return nextExecutionTime;  
            
            // 使用PerodicTrigger触发器,修改timer变量指定操作间隔,单位为毫秒
            // PeriodicTrigger periodicTrigger = new PeriodicTrigger(timer);  
            // Date nextExecutionTime = periodicTrigger.nextExecutionTime(triggerContext);  
            // return nextExecutionTime;  
        }  
        });  
    }  
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

办了房屋抵押经营贷,空壳公司不怕被查吗?续贷不上怎么办?

很多有房的朋友&#xff0c;想必都办理过抵押经营贷款。但是&#xff0c;当办完房屋抵押经营贷款之后&#xff0c;钱到手了&#xff0c;别光顾着乐呵&#xff0c;贷后管理可是门大学问&#xff0c;稍有不慎&#xff0c;麻烦就找上门了。咱得确保资金用得对路&#xff0c;征信亮…

零宽字符应用场景及前端解决方案

零宽字符&#xff08;Zero Width Characters&#xff09;是一类在文本中不可见但具有特定功能的特殊字符。称为零宽字符&#xff0c;也叫幽灵字符。它们在显示时不占据任何空间&#xff0c;但在文本处理和显示中发挥着重要作用。这些字符主要包括零宽度空格、零宽度非连接符、零…

2024 VMpro 虚拟机中如何给Ubuntu Linux操作系统配置联网

现在这是一个联网的状态 可以在商店里面下载东西 也能ping成功 打开虚拟网络编辑器 放管理员权限 进行设置的更改 选择DNS设置 按提示修改即可 注意的是首选的DNS服务器必须是114.114.114.114 原因 这边刚刚去查了一下 114.114.114.114 是国内的IP地址 8.8.8.8 是国外的I…

鸿蒙媒体开发系列04——音频播放

如果你也对鸿蒙开发感兴趣&#xff0c;加入“Harmony自习室”吧&#xff01;扫描下方名片&#xff0c;关注公众号&#xff0c;公众号更新更快&#xff0c;同时也有更多学习资料和技术讨论群。 1、如何选择音频播放开发方式 在HarmonyOS系统中&#xff0c;多种API都提供了音频播…

QUIC的loss detection学习

PTO backoff backoff 补偿 /ˈbkɒf/PTO backoff 是QUIC&#xff08;Quick UDP Internet Connections&#xff09;协议中的一种机制&#xff0c;用于处理探测超时&#xff08;Probe Timeout, PTO&#xff09;重传策略 它逐步增加探测超时的等待时间&#xff0c;以避免网络拥塞…

Web开发:使用C#创建、安装、调试和卸载服务

目录 一、创建服务 1.创建项目&#xff08;.NET Framework&#xff09; 2.重命名 3.编写逻辑代码 二、安装服务 1.方案一&#xff1a;利用VS2022安装文件的配置 选择添加安装程序 安装文件的介绍及配置 ​编辑​ 重新编译 工具安装 2.方案二&#xff1a;编写bat脚本安…

【嘉立创EDA】画PCB板中为什么要两面铺铜为GND,不能一面GND一面VCC吗?

在新手画板子铺铜时&#xff0c;经常会铺一面GND一面VCC。但一般情况下我们不会这样铺铜。下面将详细分析为什么要两面铺铜为GND&#xff0c;而不是一面GND一面VCC的原因&#xff1a; 提高散热能力 金属导热性&#xff1a;金属具有良好的导热性&#xff0c;铺铜可以有效分散PCB…

Oracle 19c异常恢复—ORA-01209/ORA-65088---惜分飞

由于raid卡bug故障,导致文件系统异常,从而使得数据库无法正常启动,客户找到我之前已经让多人分析,均未恢复成功,查看alert日志,发现他们恢复的时候尝试resetlogs库,然后报ORA-600 kcbzib_kcrsds_1错误 2024-09-15T17:07:32.55321508:00 alter database open resetlogs 2024-09-…

Debian11之DolphinScheduler使用

登录 默认用户名和密码 admin/dolphinscheduler123 http://192.168.111.180:12345/dolphinscheduler/ui基础配置 1、创建Worker【admin用户下】 创建项目的时候会指定Worker&#xff0c;这个配置决定了项目中的任务在哪个服务器执行 2、创建环境【admin用户下】 - 如果涉…

Linux搭建邮箱服务器(简易版)

本章是上一文档的简易版本搭建方式更为快速简洁&#xff08;只需要两条命令即可搭建&#xff09;&#xff0c;如果想了解更详细一些可以看我上一文档 Linux接发邮件mailx_linux mailx o365-CSDN博客文章浏览阅读857次&#xff0c;点赞25次&#xff0c;收藏19次。本文详细描述了…

spring security OAuth2 搭建资源服务器以及授权服务器/jdbc/jwt两种方案

一、认证服务器基于jdbc方式 如果不懂请移步上一篇文章&#xff1a;Spring security OAuth2 授权服务器搭建-CSDN博客 在上一篇文章中&#xff0c;TokenStore的默认实现为 InHenoryTokenStore 即内存存储&#xff0c;对于 CLient 信息&#xff0c;userDetaitsServce 接负责从存…

mqtt整体了解

整个系统的分布及功能 参考太极创客视频 整体分为三部分&#xff1a; 发布&#xff1a;实时发送到云平台&#xff1b;实现主体是传感器或被控对象 订阅&#xff1a;得到能够访问发布信息&#xff1b;主体是有查看和控制权限的对象 云平台&#xff1a;可以理解为有控制订阅者权…

Python 爬虫入门 - Request 静态页面数据获取

在现代 Web 开发中,HTTP 请求(Request)是与服务器进行通信的核心操作。无论是在前端还是后端开发中,数据的获取、传递以及处理都离不开请求的应用。特别是在静态页面的数据获取中,使用请求可以将页面变得更加动态和互动,从而大大提升用户体验,使得页面内容更加丰富和灵活…

MySQL_SQLYog简介、下载及安装(超详细)

课 程 推 荐我 的 个 人 主 页&#xff1a;&#x1f449;&#x1f449; 失心疯的个人主页 &#x1f448;&#x1f448;入 门 教 程 推 荐 &#xff1a;&#x1f449;&#x1f449; Python零基础入门教程合集 &#x1f448;&#x1f448;虚 拟 环 境 搭 建 &#xff1a;&#x1…

行人动作行为识别系统源码分享

行人动作行为识别检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer…

Halo 开发者指南——项目运行、构建

准备工作 环境要求 OpenJDK 17 LTSNode.js 20 LTSpnpm 9IntelliJ IDEAGitDocker&#xff08;可选&#xff09; 名词解释 工作目录 指 Halo 所依赖的工作目录&#xff0c;在 Halo 运行的时候会在系统当前用户目录下产生一个 halo-next 的文件夹&#xff0c;绝对路径为 ~/ha…

网络高级项目( 基于webserver的工业数据采集和控制项目)

目录 一、项目要求&#xff1a; 二、演示效果&#xff1a; 设备端&#xff1a; Modbus用户控制端&#xff1a; 服务器端&#xff1a; 网页端&#xff1a; 三、 项目代码&#xff1a; Modbus用户控制端代码&#xff1a; 服务器端代码&#xff1a; 网页端代码&#xff1…

VirtualBox Install MacOS

环境搭建 git clone https://github.com/myspaghetti/macos-virtualbox 脚本配置 修改macos-guest-virtualbox.sh部分内容为 vm_name"macOS" # name of the VirtualBox virtual machine macOS_release_name"Catalina" # install &quo…

股指期货的详细玩法功能与应用解析

股指期货作为一种重要的金融衍生工具&#xff0c;为投资者提供了多样化的投资和风险管理手段。本文将详细探讨股指期货的三大主要功能&#xff1a;风险规避、价格发现和资产配置。 第一&#xff0c;风险规避功能 1.套期保值&#xff1a;股指期货的风险规避功能主要通过套期保值…

外观模式详解:如何为复杂系统构建简洁的接口

&#x1f3af; 设计模式专栏&#xff0c;持续更新中 欢迎订阅&#xff1a;JAVA实现设计模式 &#x1f6e0;️ 希望小伙伴们一键三连&#xff0c;有问题私信都会回复&#xff0c;或者在评论区直接发言 外观模式 外观模式&#xff08;Facade Pattern&#xff09;为子系统中的一组…