Spring Task定时任务(基于@Scheduled注解)

目录

一.Spring Task是什么?它的作用?

二.使用步骤

1.pom依赖引入

2.在启动类上添加@EnableScheduling注解

3.@Schedule注解

        3.1 cron表达式

   特殊字符说明

                注意事项

               实例代码

Cron表达式生成工具

​        3.2 fixedDelay

        3.3 fixedRate

四.线程阻塞问题

五.线程阻塞的解决办法

@Async注解使用

总结


一.Spring Task是什么?它的作用?

Spring Task是Spring框架中提供的一种任务调度机制,用于执行计划任务和定期任务。他能够在指定的时间间隔内执行任务,而无需手动控制任务的启动和结束。

它的作用主要是体现在比如我们要定期执行任务,或者我们定期更新数据库里面的数据等这些场景里面就会使用到Spring Task。

二.使用步骤

1.pom依赖引入

只要你的工程是基于spring boot来创建的,那么就能直接使用,无需引入额外的依赖,因为它是包含在spring起步依赖里面的

2.在启动类上添加@EnableScheduling注解

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling//启用任务调度
public class SpringTaskApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringTaskApplication.class, args);
    }

}

添加这个注解在启动类上面的作用是开启Spring Task的定时任务调度功能,这个注解会向Spring容器注册与定时任务相关的组件如ScheduledAnnotationBeanPostProcessor,我们通过源码可以观察到

首先进入到@EnableScheduling中它使用@Import注解显示的导入了SchedulingConfiguration这个配置类(@Import注解的其中一个作用就是将其他@Configuration类中定义的 Bean 合并到当前上下文中),然后我们点击这个配置类,会发现它的ScheduledAnnotationBeanPostProcessor方法添加了bean注解在项目启动时候会调用到这个方法来new ScheduledAnnotationBeanPostProcessor这么一个对象(再深的就不做描述了),它最后就会帮我们去扫描所有Bean中的@Scheduled注解,发现带有@Scheduled注解的方法就会将其注册到TaskScheduler中。

TaskScheduler是任务调度的核心接口它定义了多种调度方式(fixedRate,fixedDelay,Cron 表达式),还统一管理任务的生命周期。

有人就会问了,你说这么多,那他的作用我还是没有搞懂。

其实我们从TaskScheduler 与@Scheduled注解的关系来看就比较清晰了:当使用@Scheduled注解标记方法时,Spring底层会通过 TaskScheduler 的实现类(默认实现类如ThreadPoolTaskScheduler)来调度这些任务。如果没有显示创建一个TaskScheduler那么Spring 会自动创建一个单线程的ThreadPoolTaskScheduler,可能导致任务阻塞,此时就要修改它默认的单例线程的配置(在配置文件里面修改)。

3.@Schedule注解

我们此时就要在我们自己创建的Spring Task类的方法上面去添加@Scheduled注解

注意我们还要在这个Spring Task类上面添加@Component注解,在spring扫描到带有@Component注解的spring task类的时候将其注册为Bean,而Spring会将其实例化,然后 后处理器再介入扫描带有@Scheduled的方法并将每个方法封装为一个任务交给TaskScheduler任务调度器执行

        3.1 cron表达式

          在Spring Task中@Scheduled注解的参数用于指定基于cron表达式的复杂定时规则,而Cron表达式时一种字符串格式,由6个或者7个字段组成其顺序为

秒 分 时 日 月 周 年(可选)

为什么他要把月放在周的前面我也不清楚,这里有点不一样,在这里面年是可以不用指定的。

每个字段的取值范围及含义如下:

字段范围特殊字符说明
0~59, - * /

0~59, - * /
0~23, - * /
1~31, - * / ? L W
1~12或者JAN~DEC, - * /

0~7或SUN~SAT

, - * / ? L #
1970~2099, - * /

特殊字符说明
字符作用示例
*所有值* 在秒字段表示每秒触发
?不指定值(仅在“日”和“周几”字段有效,表示冲突字段忽略)0 0 0 ? * MON
-区间范围10-20 秒表示10~20秒
,多个值MON,WED,FRI 周几
/步长(从起始值开始每隔多少单位触发)0/5 秒表示每5秒
L最后一天(仅在“日”和“周几”字段有效)L 在日字段表示月末
W最近工作日(仅在“日”字段有效,如 15W 表示离15号最近的工作日)15W
#第几个周几(仅在“周几”字段有效,如 6#3 表示每月的第三个周五)FRI#3
 注意事项

若是同时指定了"日"和"周",则需要将其中一个字段写为 ? 否则会由歧义,打比方说我指定了每个月的1号执行这个方法,那么我就不会再去指定周了,因为没有意义,你指定了周几的话,那万一1号那天不是你指定的那个周几呢?所以他们时会有冲突的

在月里面使用W的话如1W指的是距离一号最近的工作日,但是这个工作日只能在当日里面,不能往前面看

 实例代码
@Component
public class TestSpringTask {
    @Scheduled(cron = "0/5 * * * * *")//指定每隔五秒就触发一次
    public void test(){
        System.out.println("触发了,当前时间"
                + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }
}

控制台输出

Cron表达式生成工具

语法有时候会忘记但是网上有Cron表达式转化器可以直接生成一个需要的Cron表达式

网站:

​        3.2 fixedDelay

 其作为@Scheduled的参数之一,用于定义一种固定延迟执行的任务规则。他表示前一次任务执行结束后,会间隔指定时间再执行下一次任务,他的单位是毫秒如fixedDelay=5000就代表时间间隔为5秒

实例代码

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import static java.lang.Thread.sleep;

@Component
public class TestSpringTask {
    @Scheduled(fixedDelay = 5000)//指定每隔五秒就触发一次,初始是从第0秒开始触发
    public void test() throws InterruptedException {
        System.out.println("触发了,当前时间"
                + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        sleep(5000);
        System.out.println("方法执行完成,当前时间:"+LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }
}

控制台输出

注意到方法中调用了sleep休眠函数,停止了五秒,然后由于fixedDelay设置的5秒,他会在方法调用完之后停五秒再调用这个方法

        3.3 fixedRate

fixedRate参数定义了一种固定频率执行的定时任务规则。他表示按照固定的时间间隔触发任务,即上一次方法开始到下一次方法开始之间的时间

@Component
public class TestSpringTask {
    @Scheduled(fixedRate = 5000)
    public void test() throws InterruptedException {
        System.out.println("触发了,当前时间"
                + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        sleep(3000);
        System.out.println("方法执行完成,当前时间:"+LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }
}

控制台输出

我们可以看到当方法执行完之后他会等到这个间隔完了才会触发,而不会马上去执行,这个间隔指的是从方法调用的开始计算的,即上一次开始到下一次的开始时间间隔是固定的。这里可能会出问题,因为在默认情况下Spring Boot定时任务是单线程执行的。

四.线程阻塞问题

默认情况下这些定时任务是以单线程的方式执行所有任务的

我们先以cron表达式的方式来进行问题描述,当我们设置了两个任务来执行,他是以同频的方式来执行的,但是其中一个他的执行时间超过了时间频率那么就会出问题

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import static java.lang.Thread.sleep;


@Component
public class TestSpringTask {
    @Scheduled(cron = "0/1 * * * * *")
    public void test01() throws InterruptedException {
        System.out.println("=========================");
        System.out.println("这是test01方法");
        System.out.println("当前时间"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        System.out.println(Thread.currentThread().getName());//获取当前线程的id
        System.out.println("=========================");
        sleep(5000);
    }
    @Scheduled(cron = "0/1 * * * * *")
    public void test02(){
        System.out.println("=========================");
        System.out.println("这是test02方法");
        System.out.println("当前时间"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        System.out.println(Thread.currentThread().getName());//获取当前线程的id
        System.out.println("=========================");
    }
}

在这个示例里面我们把两个方法设置成同频的方式执行,他的执行结果如图

test02方法并不会如愿的执行,因为这个线程默认是单线程的,test02会在队列里面排队等待这个test01执行完毕释放这个线程资源才会去执行(因为使用了同一个线程),这是一个很严重的问题

在使用fixedRate的时候也会出现这个问题

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import static java.lang.Thread.sleep;


@Component
public class TestSpringTask {
    @Scheduled(cron = "0/1 * * * * *")
    public void test01() throws InterruptedException {
        System.out.println("=========================");
        System.out.println("这是test01方法");
        System.out.println("当前时间"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        System.out.println(Thread.currentThread().getName());//获取当前线程的id
        System.out.println("=========================");
        sleep(5000);
    }
    @Scheduled(fixedRate = 2000)
    public void test02(){
        System.out.println("=========================");
        System.out.println("这是test02方法");
        System.out.println("当前时间"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        System.out.println(Thread.currentThread().getName());//获取当前线程的id
        System.out.println("=========================");
    }
}

这里就能发现fixedRate我们设置的时间是2s也就意味着上一方法开始到下一方法的开始间隔为2s,但是由于test01的执行时间超过了这个设置的间隔,他并没有释放线程资源,那么test02也就不会按照规定的时间执行。

五.线程阻塞的解决办法

我们要在配置文件里面修改线程的数目

spring.task.scheduling.thread-name-prefix=MyTaskThread_
#这个是设置线程的前缀名
spring.task.scheduling.pool.size=2
#设置task线程的数量  默认为1

此时我们再来观察

可以观察到他们的执行顺序和线程名发生了变化,说明此时他们就是以不同的线程来执行任务的,他们互不干扰。

在设置这些配置的时候我们往往还会带着另外两个配置

spring.task.scheduling.shutdown.await-termination=false
#线程关闭前是否等待所有任务执行完毕
spring.task.scheduling.shutdown.await-termination-period=10s
#线程关闭前最大执行时间

spring.task.scheduling.shutdown.await-termination=false

这个作用就是决定在spring boot项目关闭的时候是否等待正在执行的任务,true等待 调度器就会等所有任务执行完之后再关闭

spring.task.scheduling.shutdown.await-termination-period=10s
这个是设置的在项目关闭的时候允许等待的最大时间,若为10s则代表最多等10秒

这里简单介绍一下@Async注解,他是方法异步执行的注解(主要是引出他的配置文件),他的独特点在于能单开一个线程来执行方法,而不是依赖于其他线程资源释放。

@Async注解使用

配置类设置:用于创建异步线程池

@Configuration
public class AsyncConfig {
    
    @Bean
    public Executor taskExecutor() {
        return Executors.newFixedThreadPool(10); // 创建一个线程池,最大线程数为10
    }
}

异步任务方法

@Component
public class AsyncTask {

    @Async
    public void performAsyncTask() {
        System.out.println("异步任务正在执行: " + LocalDateTime.now());
    }
}

配置文件设置




# 任务执行线程池配置

# 是否允许核心线程超时。这样可以动态增加和缩小线程池
spring.task.execution.pool.allow-core-thread-timeout=true
#  核心线程池大小 默认 8
spring.task.execution.pool.core-size=8
# 线程空闲等待时间 默认 60s
spring.task.execution.pool.keep-alive=60s
# 线程池最大数  根据任务定制
spring.task.execution.pool.max-size=
#  线程池 队列容量大小
spring.task.execution.pool.queue-capacity=
# 线程池关闭时等待所有任务完成
spring.task.execution.shutdown.await-termination=true
# 执行线程关闭前最大等待时间,确保最后一定关闭
spring.task.execution.shutdown.await-termination-period=
# 线程名称前缀
spring.task.execution.thread-name-prefix=task-

总结

通过这篇文章我们就能够大概了解Spring Task的使用流程以及会出现的问题,但是注意SpringTask并适合用在分布式环境的,在分布式环境下,这种定时任务是不支持集群配置的,若部署到多个节点上,各个节点没有任何通讯机制,那么就不会共享任务信息,导致任务在每个节点上都会被执行,导致任务重复,但是可通过Quartz,xxljob等定时任务调度框架来执行,或者借助redis等来实现分布式锁处理各个节点协调问题,如果有感兴趣的我会出两期期来讲定时任务调度框架和分布式锁实现的方法。

最后

本人的第三篇博客,以此来记录我的后端java学习。如文章中有什么问题请指出,非常感谢!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值