SpringBoot教程(二十一) | SpringBoot实现单点定时任务之@Scheduled

前言

在Spring Boot项目中使用@Scheduled注解实现定时任务时,你通常不需要额外导入特定的依赖,
因为@Scheduled是Spring框架的核心功能之一,并且Spring Boot会自动配置与调度相关的组件。

@EnableScheduling:Spring框架提供的一个注解,用于启用基于注解的定时任务调度功能。当在Spring的配置类(如使用@Configuration注解的类)上使用@EnableScheduling注解时,Spring会自动配置一个任务调度器(TaskScheduler),负责管理所有带有@Scheduled注解的方法。

@Scheduled:Spring框架中用于定时任务调度的注解。它允许开发者将一个方法标记为定时任务,并配置任务的执行时间间隔或Cron表达式,从而实现在指定时间或按照指定周期自动执行该方法的功能。 除了配置Cron表达式外,还可以通过fixedRate和fixedDelay两种方式设置定时任务,这两种方式可以自行了解。

@Slf4j
@Component
@EnableScheduling
public class DemoTask {
 
    // 每5秒执行一次
    @Scheduled(cron = "0/5 * * * * ? ")
    public void testSchedule1() {
        log.info("第 1 个定时任务"+"当前执行任务的线程号ID===>{}", Thread.currentThread().getId()); // 日志输出
    }

    // 每10秒执行一次
    @Scheduled(cron = "0/10 * * * * ? ")
    public void testSchedule2() {
        log.info("第 2 个定时任务"+"当前执行任务的线程号ID===>{}", Thread.currentThread().getId()); // 日志输出
    }
 
}

项目启动后
在这里插入图片描述
是成功且正常执行的
当我把阻塞sleep加进去以后

@Slf4j
@Component
@EnableScheduling
public class DemoTask {
 
    // 每10秒执行一次
    @Scheduled(cron = "0/10 * * * * ? ")
    public void testSchedule1() {
         Thread.sleep(10000);//休眠10秒
        log.info("第 1 个定时任务"+"当前执行任务的线程号ID===>{}", Thread.currentThread().getId()); // 日志输出
    }

    // 每10秒执行一次
    @Scheduled(cron = "0/10 * * * * ? ")
    public void testSchedule2() {
        log.info("第 2 个定时任务"+"当前执行任务的线程号ID===>{}", Thread.currentThread().getId()); // 日志输出
    }
 
}

在这里插入图片描述

会发现怎么定时任务2变成间隔20秒执行一次了
是因为会执行这个定时任务使用的线程号ID都是同一个,任务1堵塞了10秒导致影响了后面任务2的执行(说明都用了同一个线程去执行定时任务的,简直巨坑!!!)。

巨坑(@Scheduled任务都用了同一个线程去执行,导致定时任务存在堵塞)

解决办法一:添加自定义的ThreadPoolTaskScheduler配置(为调度配置多个线程)

默认情况下,Spring会尝试在Spring应用上下文中查找一个名为taskScheduler的bean,这个bean必须是TaskScheduler接口的实现。
一旦找到了这个bean,Spring就会使用它来调度所有@Scheduled注解标记的方法。
所以Bean的name不能随便乱写

import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;  
  
@Configuration  
public class SchedulerConfig {  
     
    //这个bean名字不要乱改,否则会不生效
    @Bean(name = "taskScheduler")  
    public ThreadPoolTaskScheduler taskScheduler() {  
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();  
        // 设置线程池的核心大小  
        scheduler.setPoolSize(10);  
        // 其他设置,如线程名称前缀等(可选)  
        scheduler.setThreadNamePrefix("wocao-task-");  
        // 初始化调度器  
        scheduler.initialize();  
        return scheduler;  
    }  
}

效果如下:

定时任务1不会堵塞定时任务2了,且 定时任务 都是10秒钟执行一次,不会存在堵塞延迟

在这里插入图片描述

解决办法二(建议用这个):使用异步包裹定时任务

(1)首先配置自定义线程池

@Configuration
public class DemoTheadPoolConfig {

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

(2) 执行异步操作

方法一:使用工具类CompletableFuture.runAsync

CompletableFuture.runAsync 是 Java 8 引入的一个非常有用的工具,用于异步执行任务
runAsync 方法用于启动一个异步任务,这个任务不返回结果(即返回类型为 void)。

基本用法

runAsync 方法有两种形式:

  1. 无参数版本:使用系统默认的 ForkJoinPool 来异步执行任务。

    CompletableFuture.runAsync(() -> {
        // 这里是异步执行的任务
        System.out.println("异步任务执行中:" + Thread.currentThread().getName());
    });
    

    在这个例子中,runAsync 接收一个 Runnable 函数式接口的实现(即一个不接受参数且不返回结果的 run 方法),并在一个默认的线程池中异步执行这个任务。

  2. 带 Executor 参数版本:允许你指定一个自定义的 Executor 来执行异步任务。

    Executor executor = Executors.newFixedThreadPool(5); // 创建一个固定大小的线程池
    CompletableFuture.runAsync(() -> {
        // 这里是异步执行的任务
        System.out.println("异步任务执行中,使用自定义线程池:" + Thread.currentThread().getName());
    }, executor);
    

    在这个例子中,runAsync 除了接收一个 Runnable 任务外,还接收一个 Executor 参数,允许你控制异步任务的执行线程。

@Slf4j
@Component
@EnableScheduling
public class DemoTask {

    @Autowired
    @Qualifier("taskExecutor") // 确保使用我们自定义的线程池
    private TaskExecutor taskExecutor; // 注入 TaskExecutor
 
    // 每10秒执行一次
    @Scheduled(cron = "0/10 * * * * ? ")
    public void testSchedule1() {
        CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(10000); // 休眠10秒,模拟业务场景执行时间
                log.info("第 1 个定时任务"+"当前执行任务的线程号ID===>{}", Thread.currentThread().getId()); // 日志输出
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, taskExecutor);
    }

    // 每10秒执行一次
    @Scheduled(cron = "0/10 * * * * ? ")
    public void testSchedule2() {
        CompletableFuture.runAsync(() -> {
            try {
                log.info("第 2 个定时任务"+"当前执行任务的线程号ID===>{}", Thread.currentThread().getId()); // 日志输出
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, taskExecutor);
    }
 
}

效果如下

定时任务1不会堵塞定时任务2了,且 定时任务 都是10秒钟执行一次,不会存在堵塞延迟

在这里插入图片描述

方法二:使用@Async+@EnableAsync

@EnableAsync:开启异步任务
@Async:给希望异步执行的方法标注
一般使用@Async都会指定自定义的线程池
在此处的例子应该写成这样@Async(value = “TaskExecutor”)

@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class DemoTask {
 
    // 每10秒执行一次
    @Async("taskExecutor")//指定自定义的线程池
    @Scheduled(cron = "0/10 * * * * ? ")
    public void testSchedule1() {
        try {
            Thread.sleep(10000); // 休眠10秒,模拟业务场景执行时间
            log.info("第 1 个定时任务"+"当前执行任务的线程号ID===>{}", Thread.currentThread().getId()); // 日志输出
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

   // 每10秒执行一次
    @Async("taskExecutor")//指定自定义的线程池
    @Scheduled(cron = "0/10 * * * * ? ")
    public void testSchedule2() {
        try {
            log.info("第 2 个定时任务"+"当前执行任务的线程号ID===>{}", Thread.currentThread().getId()); // 日志输出
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

效果如下

定时任务1不会堵塞定时任务2了,且 定时任务 都是10秒钟执行一次,不会存在堵塞延迟

在这里插入图片描述

参考文章:
【1】IDEA SpringBoot实现定时任务(保姆级教程,超详细!!!)

  • 18
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值