SpringBoot项目中@Scheduled定时任务配置线程池

一、ThreadPoolTaskExecutor()实现

序言

对于定时任务,在SpringBoot中只需要使用@Scheduled 这个注解就能够满足需求,它的出现也给我们带了很大的方便,我们只要加上该注解,并且根据需求设置好就可以使用定时任务了。

但是,我们需要注意的是,@Scheduled 并不一定会按时执行

因为使用@Scheduled 的定时任务虽然是异步执行的,但是,不同的定时任务之间并不是并行的!!!!!!!!

在其中一个定时任务没有执行完之前,其他的定时任务即使是到了执行时间,也是不会执行的,它们会进行排队。

也就是如果你想你不同的定时任务互不影响,到时间就会执行,那么你最好将你的定时任务方法自己搞成异步方法,这样,定时任务其实就相当于调用了一个线程执行任务,一瞬间就结束了。比如使用:@Async

当然,也可以勉强将你的定时任务当做都会定时执行。但是,作为一个合格的程序员

那么,如何将@Scheduled实现的定时任务变成异步的呢?此时你需要对@Scheduled进行线程池配置。

 

配置示例

package com.java.navtool.business.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author :mmzsblog.cn
 * @date :Created in 2021/7/27 17:46
 * @description:spring-boot 多线程  @Scheduled注解 并发定时任务的解决方案
 * @modified By:
 * @version:
 */

@Configuration
@EnableScheduling
public class ScheduleConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(taskExecutor());
    }

    public static final String EXECUTOR_SERVICE = "scheduledExecutor";

    @Bean(EXECUTOR_SERVICE)
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
        // 设置最大线程数
        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 10);
        // 设置队列容量
        executor.setQueueCapacity(Runtime.getRuntime().availableProcessors() * 10);
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(10);
        // 设置默认线程名称
        executor.setThreadNamePrefix("scheduled-");
        // 设置拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 等待所有任务结束后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        return executor;
    }

}

附带介绍一下线程池的几个参数。需要彻底搞懂,不要死记硬背哦!

 

线程池参数

  • 1、corePoolSize(必填):核心线程数。
  • 2、maximumPoolSize(必填):最大线程数。
  • 3、keepAliveTime(必填):线程空闲时长。如果超过该时长,非核心线程就会被回收。
  • 4、unit(必填):指定keepAliveTime的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
  • 5、workQueue(必填):任务队列。通过线程池的execute()方法提交的Runnable对象将存储在该队列中。
  • 6、threadFactory(可选):线程工厂。一般就用默认的。
  • 7、handler(可选):拒绝策略。当线程数达到最大线程数时就要执行饱和策略。

说下核心线程数和最大线程数的区别:

 

拒绝策略可选值:

  • 1、AbortPolicy(默认):放弃任务并抛出RejectedExecutionException异常。
  • 2、CallerRunsPolicy:由调用线程处理该任务。
  • 3、DiscardPolicy:放弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
  • 4、DiscardOldestPolicy:放弃队列最早的未处理任务,然后重新尝试执行任务。

 

线程池执行流程:

上个流程图,先试着自己看下能不能看懂:

 

简短的总结下线程池执行流程:

  • 1、一个任务提交到线程池后,如果当前的线程数没达到核心线程数,则新建一个线程并且执行新任务,注意一点,这个新任务执行完后,该线程不会被销毁;
  • 2、如果达到了,则判断任务队列满了没,如果没满,则将任务放入任务队列;
  • 3、如果满了,则判断当前线程数量是否达到最大线程数,如果没达到,则创建新线程来执行任务,注意,如果线程池中线程数量大于核心线程数,每当有线程超过了空闲时间,就会被销毁,直到线程数量不大于核心线程数;
  • 4、如果达到了最大线程数,并且任务队列满了,就会执行饱和策略;

二、ThreadPoolTaskScheduler()

实现

使用spring的定时器 @Scheduled 的话,因为 @Scheduled 默认是单线程执行的,所以在需要的时候,我们可以设置一个线程池去执行定时任务。
1 在启动类上加入@EnableScheduling注解

@EnableScheduling
@SpringBootApplication
@Slf4j
public class SynchronizationApplication {

public static void main(String[] args) throws UnknownHostException {
SpringApplication app = new SpringApplication(SynchronizationApplication.class);
//同名Bean允许覆盖,不设置时默认为true
app.setAllowBeanDefinitionOverriding(true);
Environment env = app.run(args).getEnvironment();
//服务名称
String appName = env.getProperty("spring.application.name");
//端口号
String port = env.getProperty("server.port");
//服务对外ip地址
String ip = InetAddress.getLocalHost().getHostAddress();
}

}

2 通过实现SchedulingConfigurer接口来将定时线程池放入

@Configuration
public class TaskConfig implements SchedulingConfigurer {

@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
executor.setPoolSize(10);
executor.setThreadNamePrefix("task-thread");
//设置饱和策略
//CallerRunsPolicy:线程池的饱和策略之一,当线程池使用饱和后,直接使用调用者所在的线程来执行任务;如果执行程序已关闭,则会丢弃该任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}

//配置@Scheduled 定时器所使用的线程池
//配置任务注册器:ScheduledTaskRegistrar 的任务调度器
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
//可配置两种类型:TaskScheduler、ScheduledExecutorService
//scheduledTaskRegistrar.setScheduler(taskScheduler());
//只可配置一种类型:taskScheduler
scheduledTaskRegistrar.setTaskScheduler(taskScheduler());
}

}

线程池的饱和策略:

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了,线程池会执行饱和策略。

ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy:当线程池使用饱和后,直接使用调用者所在的线程来执行任务;如果执行程序已关闭,则会丢弃该任务。
ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
3 编写定时任务
cron在线生成网址   http://cron.ciding.cc/

//注册为spring容器的组件@Component

@Slf4j
public class SchedulerTask {

//定时任务
// 5 * * * * ? 在每分钟的5秒执行
@Scheduled(cron = " 5 * * * * ? ")
public void scheduleTask() {
try {
log.info("定时任务: 开始执行");
//todo:执行业务
log.info("定时任务: 执行完毕");
} catch (Exception e) {
log.error("定时任务执行出错", e);
}
}
}

三、ThreadPoolTaskExecutor()实现和ThreadPoolTaskScheduler()实现的对比

ThreadPoolTaskScheduler和ThreadPoolTaskExecutor
继承关系结构图:

蓝色实现的箭头是指继承关系
绿色虚线指的是接口实现关系
绿色实线指的是接口继承关系
继承关系结构
ThreadPoolTaskExecutor

ThreadPoolTaskScheduler

使用

@EnableAsync
@Configuration
public class ThreadPoolConfig {

@Bean(name = AsyncExecutionAspectSupport.DEFAULT_TASK_EXECUTOR_BEAN_NAME)
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程池大小
executor.setCorePoolSize(10);
//最大线程数
executor.setMaxPoolSize(30);
//队列容量
executor.setQueueCapacity(100);
//活跃时间
executor.setKeepAliveSeconds(60);
//线程名字前缀
executor.setThreadNamePrefix("taskExecutor-");
// 设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean
executor.setWaitForTasksToCompleteOnShutdown(true);
// 线程池对拒绝任务的处理策略,当线程池没有处理能力的时候,该策略会直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}

@Bean("taskExecutor")
public Executor taskExecutor() {
// 创建一个线程池对象
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
// 定义一个线程池大小
scheduler.setPoolSize(100);
// 线程池名的前缀
scheduler.setThreadNamePrefix("taskExecutor-");
// 设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean
scheduler.setWaitForTasksToCompleteOnShutdown(true);
// 设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住
scheduler.setAwaitTerminationSeconds(60);
// 线程池对拒绝任务的处理策略,当线程池没有处理能力的时候,该策略会直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务
scheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return scheduler;
}

}

总结
从结构上看,两者只相差了一个TaskScheduler,而TaskScheduler是专门用于调度任务的类,这也从根本上区分了两者的用途

ThreadPoolTaskExecutor是一个专门用于执行任务的类。

ThreadPoolTaskScheduler是一个专门用于调度任务的类。

从使用上看,ThreadPoolTaskExecutor可以进行更为细粒度的划分

因此,在两者之间进行选择归结为以下问题:我是否需要执行或安排任务的执行

扩展
Reject策略预定义
RejectedExecutionHandler handler,Reject策略预定义有四种:

(1)ThreadPoolExecutor.AbortPolicy策略,是默认的策略,处理程序遭到拒绝将抛出运行时 RejectedExecutionException。

(2)ThreadPoolExecutor.CallerRunsPolicy策略 ,调用者的线程会执行该任务,如果执行器已关闭,则丢弃.

(3)ThreadPoolExecutor.DiscardPolicy策略,不能执行的任务将被丢弃.

(4)ThreadPoolExecutor.DiscardOldestPolicy策略,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)

posted @ 2022-09-17 11:41  liftsail  阅读(5387)  评论(0编辑  收藏  举报