Spring定时任务和@Async注解异步调用

Spring定时任务

1、@Scheduled注解方式

使用方式

  • @Scheduled的使用方式十分简单,首先在项目启动类添加注解@EnableScheduled

  • 编写定时任务方法,方法上添加注解@Scheduled

  • 如果有多个定时任务,可以使用异步或者多线程解决。

参数说明

@Scheduled(fixedRate=2000):上一次开始执行时间点后2秒再次执行;单位ms;
@Scheduled(fixedDelay=2000):上一次执行完毕时间点后2秒再次执行;单位ms;
@Scheduled(initialDelay=1000, fixedDelay=2000):第一次延迟1秒执行,然后在上一次执行完毕时间点后2秒再次执行;
@Scheduled(cron="* * * * * ?"):按cron规则执行。

注解方式总结

  • 如果是强调任务间隔的定时任务,建议使用fixedRate和fixedDelay;

  • 如果是强调任务在某时某分某刻执行的定时任务,建议使用cron表达式。

参考代码示例

@Component
public class testTask {
    private Logger logger = LoggerFactory.getLogger(testTask.class);

    @Scheduled(cron = "0/5 * * * * ?")
    public void doTask() {
        logger.info(Thread.currentThread().getName()+"===task run");
    }
}

按条件自动停止任务

@Slf4j
@Component
public class AutoStopTask {

    @Autowired
    private CustomTaskScheduler customTaskScheduler;
    private int count;

    @Scheduled(cron = "*/3 * * * * *")
    public void printTask() {
        log.info("任务执行次数:{}", count + 1);
        count++;
        // 执行3次后自动停止
        if (count >= 3) {
            log.info("任务已执行指定次数,现在自动停止");
            boolean cancelled = customTaskScheduler.getScheduledTasks().get(this).cancel(true);
            // 停止后再次启动
            if (cancelled) {
                count = 0;
                ScheduledMethodRunnable runnable = new ScheduledMethodRunnable(this, ReflectionUtils.findMethod(this.getClass(), "printTask"));
                customTaskScheduler.schedule(runnable, new CronTrigger("*/3 * * * * *"));
            }
        }
    }
}

Cron表达式参数详解

关于cron表达式的写法

如:cron="* * * * * ?"

按顺序依次为:1~7(最后一位“年”可省略)

序号 含义 设值范围 通配符
1 (0~59) , - * /
2 分钟 (0~59) , - * /
3 小时 (0~23) , - * /
4 (1~31) , - * ? / L W C
5 (1~12) , - * /
6 星期 (1~7 1=星期日 7=星期六) , - * ? / L C #
7 年份(可选) (1970-2099) , - * /

在线Cron表达式生成器:http://cron.qqe2.com/

表达式参数说明
, # 表示一个列表(1,3,5),用逗号拼接具体的时间值

- # 表示一个连续区间(9-12)

* # 代表所有可能的值

? # 仅被用于天和星期两个子表达式,表示不指定值

/ # 用来指定数值的增量(分钟里的“0/15”表示从第0分钟开始,每隔15分钟)

# # 用来指定具体的周数,"#"前面代表星期,"#"后面代表本月第几周
  # 比如"2#2"表示本月第二周的星期一,"5#3"表示本月第三周的星期四
  # 因此,"5L"这种形式只不过是"#"的特殊形式而已

L # 仅被用于天和星期两个子表达式,它是单词“last”的缩写,“L”表示这个月的最后一日,“6L”表示这个月的倒数第6天

W # 代表着*日(Mon-Fri),并且仅能用于日域中。它用来指定离指定日的最*的一个*日。
  # 大部分的商业处理都是基于工作周的,所以 W 字符可能是非常重要的。
  # 日域中的 15W 意味着离该月15号的最*一个*日。假如15号是星期六,那么 trigger 会在14号(星期五)触发,因为星期四比星期一离15号更*。

C # 代表“Calendar”的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中所有日期。
  # 例如5C在“天”字段中就相当于日历5日以后的第一天。1C在“星期”字段中相当于星期日后的第一天。

由于"月份中的日期"和"星期中的日期"这两个元素互斥的,必须要对其中一个设置“?”号
常用表达式参考
"*/5 * * * * ?"       # 每隔5秒执行一次
"0 */1 * * * ?"       # 每隔1分钟执行一次
"0 0 23 * * ?"        # 每天23点执行一次
"0 0 1 * * ?"         # 每天凌晨1点执行一次
"0 0 1 1 * ?"         # 每月1号凌晨1点执行一次
"0 0 23 L * ?"        # 每月最后一天23点执行一次
"0 0 1 ? * L"         # 每周星期天凌晨1点实行一次:
"0 26,29,33 * * * ?"  # 在26分、29分、33分执行一次
"0 0 0,3,8,21 * * ?"  # 每天的0点、3点、8点、21点执行一次
"0 0 10,14,16 * * ?"  # 每天上午10点,下午2点,4点
"0 0/30 9-17 * * ?"   # 朝九晚五工作时间内每半小时
"0 0 12 ? * WED"      # 表示每个星期三中午12点
"0 0 12 * * ?"        # 每天中午12点触发
"0 15 10 ? * *"       # 每天上午10:15触发
"0 15 10 * * ?"       # 每天上午10:15触发
"0 15 10 * * ? *"     # 每天上午10:15触发
"0 15 10 * * ?"       # 2005" 2005年的每天上午10:15触发
"0 * 14 * * ?"        # 在每天下午2点到下午2:59期间的每1分钟触发
"0 0/5 14 * * ?"      # 在每天下午2点到下午2:55期间的每5分钟触发
"0 0/5 14,18 * * ?"   # 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
"0 0-5 14 * * ?"      # 在每天下午2点到下午2:05期间的每1分钟触发
"0 10,44 14 ? 3 WED"  # 每年三月的星期三的下午2:10和2:44触发
"0 15 10 ? * MON-FRI" # 周一至周五的上午10:15触发
"0 15 10 15 * ?"      # 每月15日上午10:15触发
"0 15 10 L * ?"       # 每月最后一日的上午10:15触发
"0 15 10 ? * 6L"      # 每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6#3"     # 每月的第三个星期五上午10:15触发
"0 15 10 ? * 6L 2002-2005" # 2002年至2005年的每月的最后一个星期五上午10:15触发

2、执行多个定时任务

2.1、通过@Async注解异步调用

在项目启动类添加注解@EnableAsync

注:@Async所修饰的函数不要定义为static类型,这样异步调用不会生效

创建自定义线程池,使用线程池异步执行多个任务

异步调用
@Component
public class Task {

    @Async(value ="myPoolTaskExecutor")
    public void doTaskOne() throws Exception {
        // 同上内容,省略
    }
    @Async(value ="myPoolTaskExecutor")
    public void doTaskTwo() throws Exception {
        // 同上内容,省略
    }
    @Async(value ="myPoolTaskExecutor")
    public void doTaskThree() throws Exception {
        // 同上内容,省略
    }

    /**
    * 创建自定义线程池,提供异步调用时使用
    **/
    @Bean(name = "myPoolTaskExecutor")
    public ThreadPoolTaskExecutor getMyPoolTaskExecutor() {

        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        //核心线程数
        taskExecutor.setCorePoolSize(10);
        //线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程
        taskExecutor.setMaxPoolSize(100);
        //缓存队列
        taskExecutor.setQueueCapacity(50);
        //许的空闲时间,当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
        taskExecutor.setKeepAliveSeconds(200);
        //异步方法内部线程名称
        taskExecutor.setThreadNamePrefix("poolTestThread-");
        /**
         * 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略
         * 通常有以下四种策略:
         * ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
         * ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
         * ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
         * ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用 execute() 方法,直到成功
         */
        // 拒绝策略
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        taskExecutor.initialize();

        System.out.println("@Async 业务处理线程配置成功,核心线程池:[{}],最大线程池:[{}],队列容量:[{}],线程名称前缀:[{}]");
        return taskExecutor;
    }
}
异步回调(扩展知识)
@Async(value ="myPoolTaskExecutor")
public Future<String> doTaskOne() throws Exception {
    System.out.println("开始做任务一");
    long start = System.currentTimeMillis();
    Thread.sleep(random.nextInt(10000));
    long end = System.currentTimeMillis();
    System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
    return new AsyncResult<>("任务一完成");
}
//注意:调用方与被调方不能在同一个类

@Test
public void test() throws Exception {
    long start = System.currentTimeMillis();
    Future<String> task1 = task.doTaskOne();   //异步任务1
    Future<String> task2 = task.doTaskTwo();   //异步任务2
    Future<String> task3 = task.doTaskThree(); //异步任务3
    while(true) {
        if(task1.isDone() && task2.isDone() && task3.isDone()) {
            // 三个任务都调用完成,处理点其它事,退出循环等待
            break;
        }
        Thread.sleep(1000);
    }
    long end = System.currentTimeMillis();
    System.out.println("任务全部完成,总耗时:" + (end - start) + "毫秒");
}
@Async不生效的解决方法

1、检查是否配置相关注解
在需要用到 @Async 注解的类上加上 @EnableAsync,或者直接加在springboot启动类上;

2、在同一个类中调用需要先获取代理对象,也就是手动获取对象。

因为 @Async 注解是基于Spring AOP (面向切面编程)的,而AOP的实现是基于动态代理模式实现的。有可能因为调用方法的是对象本身而不是代理对象,因为没有经过Spring容器。

public class TestController{

    @GetMapping("/testAsync")
    public String testAsync() throws InterruptedException {
        // 手动获取代理对象
        SpringUtil.getBean(TestController.class).syncData();
        return "测试异步完成";
    }

    @Async
    public void syncData() throws InterruptedException {
        System.out.println("异步方法执行......" + Thread.currentThread().getId());
    }
}

点击查看手动获取代理对象代码实现

3、不同的类调用,直接注入即可

public class TestController{
    @Autowired
    private TestService testService;

    @GetMapping("/testAsync")
    public String testAsync() throws InterruptedException {
        testService.syncData();
        return "测试异步完成";
    }
}

public class TestServiceImpl{
    @Async
    public void syncData() throws InterruptedException {
        System.out.println("异步方法执行......" + Thread.currentThread().getId());
    }
}
@Async产生的异常问题

SpringBoot使用@Async导致的循环依赖报错的解决方案

SpringBoot使用@Async注解的8大陷阱

2.2、通过实现SchedulingConfigurer接口

Spring 中,创建定时任务除了使用@Scheduled 注解外,还可以使用 SchedulingConfigurer。

@Schedule 注解有一个缺点,其定时的时间不能动态的改变,而基于 SchedulingConfigurer 接口的方式可以做到。

点击查看基于Spring的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());
    }
}

编写定时任务

//注册为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);
        }
    }
}

3、Spring线程池和Jdk线程池

  • jdk线程池就是使用jdk线程工具类ThreadPoolExecutor 创建线程池
  • spring线程池就是使用自己配置的线程池,然后交给spring处理,可以采用Async,也可以引入线程池的bean依赖。

记一下线程池优化不当导致的资源耗尽

3.1、JDK普通线程池

由于该接口访问频率很频繁,每次访问会创建新的线程池,另外接口的循环内部不断创建线程处理,导致访问次数多了系统内部资源耗尽。

public void taskTest(){
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    CountDownLatch countDownLatch = new CountDownLatch(subjectList.size());
    try {
        for (Subject subject : subjectList) {
            Member finalMember = member;
            executorService.execute(() -> {
                //耗时操作...
                content.add(subjectMap);
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        executorService.shutdown();
    }
}

3.2、JDK普通线程池(优化)

不用每次创建新的线程池

public class ThreadPoolTest {
    private  final Logger logger=  LoggerFactory.getLogger(ThreadPoolTests.class);
    //JDK普通线程池
    private ExecutorService executorService= Executors.newFixedThreadPool(10);
    private void sleep(long m){
        try {
            Thread.sleep(m);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void testExecutorService(){
        Runnable task=new Runnable() {
            @Override
            public void run() {
                logger.debug("Hello ExecutorService");
            }
        };
        for(int i=0;i<10;i++){
            //调用submit方法,线程池会分配一个线程进行执行这个任务
            executorService.submit(task);
        }
        sleep(10000);
    }
}

3.3、Spring默认的线程池SimpleAsyncTaskExecutor

Spring异步线程池的接口类是TaskExecutor,本质还是java.util.concurrent.Executor

在没有配置的情况下,默认使用的是simpleAsyncTaskExecutor。(Spring提供了7个线程池的实现,感兴趣的可以自行了解)

特点

每次执行任务时,它会重新启动一个新的线程,并允许开发者控制并发线程的最大数量(concurrencyLimit),从而起到一定的资源节流作用。

默认是concurrencyLimit取值为-1,即不启用资源节流。

3.4、Spring自带的线程池ThreadPoolTaskExecutor

ThreadPoolTaskExecutor类,其本质是对java.util.concurrent.ThreadPoolExecutor的包装。这个类则是spring包下的,是Spring为我们开发者提供的线程池类。

Spring提供了xml给我们配置ThreadPoolTaskExecutor线程池,但是现在普遍都在用SpringBoot开发项目,所以直接上yaml或者properties配置即可,或者也可以使用@Configuration配置也行,下面演示配置和使用:

配置:application.properties
# 核心线程池数
spring.task.execution.pool.core-size=5
# 最大线程池数
spring.task.execution.pool.max-size=10
# 任务队列的容量
spring.task.execution.pool.queue-capacity=5
# 非核心线程的存活时间
spring.task.execution.pool.keep-alive=60
# 线程池的前缀名称
spring.task.execution.thread-name-prefix=线程前缀名
配置:application.yaml
spring:
  task:
    execution:
      pool:
        #核心线程数
        core-size: 5
        #最大线程数
        max-size: 20
        #任务队列容量
        queue-capacity: 10
        #非核心线程存活时间
        keep-alive: 60
      #线程池前缀名称
      thread-name-prefix: 线程前缀名
配置Config类
@Configuration
public class AsyncScheduledTaskConfig {

    @Value("${spring.task.execution.pool.core-size}")
    private int corePoolSize;
    @Value("${spring.task.execution.pool.max-size}")
    private int maxPoolSize;
    @Value("${spring.task.execution.pool.queue-capacity}")
    private int queueCapacity;
    @Value("${spring.task.execution.thread-name-prefix}")
    private String namePrefix;
    @Value("${spring.task.execution.pool.keep-alive}")
    private int keepAliveSeconds;

    @Bean
    public ThreadPoolTaskExecutor threadPoolTaskExecutorInit() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //最大线程数
        executor.setMaxPoolSize(maxPoolSize);
        //核心线程数
        executor.setCorePoolSize(corePoolSize);
        //任务队列的大小
        executor.setQueueCapacity(queueCapacity);
        //线程前缀名
        executor.setThreadNamePrefix(namePrefix);
        //线程存活时间
        executor.setKeepAliveSeconds(keepAliveSeconds);
        /**
         * 拒绝处理策略
         * CallerRunsPolicy():交由调用方线程运行,比如 main 线程。
         * AbortPolicy():直接抛出异常。
         * DiscardPolicy():直接丢弃。
         * DiscardOldestPolicy():丢弃队列中最老的任务。
         */
        // 设置拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 等待所有任务结束后再关闭线程池
        // executor.setWaitForTasksToCompleteOnShutdown(true);
        //线程初始化
        executor.initialize();
        return executor;
    }
}
线程池的使用

需在启动类加上@EnableAsync和@EnableScheduling两个注解

@Component
public class ScheduleTask {

    @Qualifier("threadPoolTaskExecutorInit")
    @Autowired
    ThreadPoolTaskExecutor executor;

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Async("threadPoolTaskExecutorInit")
    @Scheduled(fixedRate = 2000)
    public void test1() {
        try {
            Thread.sleep(6000);
            System.out.println("线程池名称:" + Thread.currentThread().getName() + "-" + sdf.format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Scheduled(cron = "*/1 * * * * ?")
    public void test2() throws ExecutionException, InterruptedException {
        CompletableFuture<String> childThread = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName()+" start,time->"+System.currentTimeMillis());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" exit,time->"+System.currentTimeMillis());
            return Thread.currentThread().getName() + "任务结束";
        },executor);
        System.out.println("child thread result->"+childThread.get());
    }
}
posted @ 2022-09-21 15:17  盗梦笔记  阅读(1146)  评论(0编辑  收藏  举报