SpringBoot @Scheduled多线程执行

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot @Scheduled多线程执行相关的知识,希望对你有一定的参考价值。

参考技术A 在用springboot框架做定时任务的时候,大部分情况都是直接通过 @Scheduled 注解来指定定时任务的。但是当你有多个定时任务时, @Scheduled 并不一定会按时执行。
因为使用 @Scheduled 的定时任务虽然是异步执行的,但是,默认不同的定时任务之间并不是并行的。

查看 org.springframework.scheduling.config.ScheduledTaskRegistrar 源码即可发现

当未手动指定 taskScheduler 时,会通过 Executors.newSingleThreadScheduledExecutor() 创建默认的单线程线程池,且该线程池的拒绝策略为 AbortPolicy ,这种策略在线程池无可用线程时丢弃任务,并抛出异常 RejectedExecutionException 。

添加配置类

接口 java.util.concurrent.RejectedExecutionHandler 提供了拒绝任务处理的自定义方法。在 java.util.concurrent.ThreadPoolExecutor 中已经包含四种拒绝策略。

springboot异步线程

前言

最近项目中出现了一个问题,发现自己的定时器任务在线上没有执行,但是在线下测试时却能执行,最后谷歌到了这篇文章SpringBoot踩坑日记-定时任务不定时了?;

本篇文章主要以自己在项目中遇到的问题为背景,并不涉及源码;

Scheduled 定时任务

Scheduled注解的具体使用方法自行百度或谷歌,这里只是使用其中的一种方式;

验证Scheduled为单线程执行

  1. 测试代码

@Component
public class TestScheduling {

    private static final Logger log= LoggerFactory.getLogger(TestScheduling.class);
    
    @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30)
    public void testOne() {
        //打印线程名字
        log.info("ThreadName:====one====" + Thread.currentThread().getName());
    }
    
    @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30)
    public void testTwo(){
        //打印线程名字
        log.info("ThreadName:====two====" + Thread.currentThread().getName());
    }
}

执行结果:


2019-11-13 16:09:07.205  INFO 26976 --- [   scheduling-1] c.example.async.timetask.TestScheduling  : ThreadName:====one====scheduling-1
2019-11-13 16:09:07.205  INFO 26976 --- [   scheduling-1] c.example.async.timetask.TestScheduling  : ThreadName:====two====scheduling-1
2019-11-13 16:09:37.206  INFO 26976 --- [   scheduling-1] c.example.async.timetask.TestScheduling  : ThreadName:====one====scheduling-1
2019-11-13 16:09:37.206  INFO 26976 --- [   scheduling-1] c.example.async.timetask.TestScheduling  : ThreadName:====two====scheduling-1

弊端

如若有多个定时器时,当其中某个任务出现死循环或一直等待时,所有的定时任务将不会执行,这就是上面说的本地定时任务能执行,线上运行久了,定时任务就不会执行了;

解决方案

1. 异步注解@Async

在所执行的定时任务的方法(方法为public)上添加@Async注解,@Async注解的具体用法自行百度或谷歌,这里使用其中的一种方式。

  1. 代码

@Component
public class TestScheduling {

    private static final Logger log = LoggerFactory.getLogger(TestScheduling.class);
    
    @Async
    @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30)
    public void testOne() {
        //打印线程名字
        log.info("ThreadName:====one====" + Thread.currentThread().getName());
    }
    
    @Async
    @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30)
    public void testTwo() {
        //打印线程名字
        log.info("ThreadName:====two====" + Thread.currentThread().getName());
    }
}
  1. 结果

2019-11-13 16:59:16.394  INFO 36964 --- [cTaskExecutor-2] c.example.async.timetask.TestScheduling  : ThreadName:====one====SimpleAsyncTaskExecutor-2
2019-11-13 16:59:16.394  INFO 36964 --- [cTaskExecutor-1] c.example.async.timetask.TestScheduling  : ThreadName:====two====SimpleAsyncTaskExecutor-1
2019-11-13 16:59:46.391  INFO 36964 --- [cTaskExecutor-3] c.example.async.timetask.TestScheduling  : ThreadName:====two====SimpleAsyncTaskExecutor-3
2019-11-13 16:59:46.391  INFO 36964 --- [cTaskExecutor-4] c.example.async.timetask.TestScheduling  : ThreadName:====one====SimpleAsyncTaskExecutor-4

可以发现,线程虽然不是同一个线程了,但是定时任务每一次执行都是新创建的线程,用完之后就销毁,这样频繁的创建销毁线程是很费资源的;

怎么发现线程被销毁,在Windows系统上可以使用Java自带的jvisualvm.exe工具来查看应用的运行状况;

2. 使用异步注解+线程池

为什么这么说,百度谷歌了很多文章,都是创建了线程池,最后异步注解却没有用创建的线程池。

  1. 创建线程池
    创建线程池的方式很多种,这里使用其中的一种:

@Configuration
public class AsyncConfig {
    @Bean
    public AsyncTaskExecutor asyncTaskExecutor() {
    
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
        executor.setThreadNamePrefix("courses-schedule-");
        //最大线程数10:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
        executor.setMaxPoolSize(10);
        //核心线程数3:线程池创建时候初始化的线程数
        executor.setCorePoolSize(3);
        //缓冲队列0:用来缓冲执行任务的队列
        executor.setQueueCapacity(5);
        //允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
        executor.setKeepAliveSeconds(60);
        // 当线程池已满,且等待队列也满了的时候,直接抛弃当前线程(不会抛出异常)
//        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        executor.initialize();
        return executor;
    }
}

这里的线程池名字为:asyncTaskExecutor,就是方法名;其中线程池里面的属性根据自己的情况而定;

这一步很多文章都写到了,而且创建线程池的方式也各种各样;

  1. 注解上使用线程池

@Component
public class TestScheduling {

    private static final Logger log = LoggerFactory.getLogger(TestScheduling.class);
    
    @Async("asyncTaskExecutor")
    @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30)
    public void testOne() {
        //打印线程名字
        log.info("ThreadName:====one====" + Thread.currentThread().getName());
    }
    
    @Async("asyncTaskExecutor")
    @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30)
    public void testTwo() {
        //打印线程名字
        log.info("ThreadName:====two====" + Thread.currentThread().getName());
    }
}

注意: 这里在@Async注解里面添加了线程名字为asyncTaskExecutor

  1. 执行结果

2019-11-13 17:22:52.021  INFO 5448 --- [rses-schedule-2] c.example.async.timetask.TestScheduling  : ThreadName:====one====courses-schedule-2
2019-11-13 17:22:52.021  INFO 5448 --- [rses-schedule-3] c.example.async.timetask.TestScheduling  : ThreadName:====two====courses-schedule-3
2019-11-13 17:23:22.021  INFO 5448 --- [rses-schedule-1] c.example.async.timetask.TestScheduling  : ThreadName:====two====courses-schedule-1
2019-11-13 17:23:22.021  INFO 5448 --- [rses-schedule-2] c.example.async.timetask.TestScheduling  : ThreadName:====one====courses-schedule-2
2019-11-13 17:23:52.021  INFO 5448 --- [rses-schedule-3] c.example.async.timetask.TestScheduling  : ThreadName:====two====courses-schedule-3
2019-11-13 17:23:52.021  INFO 5448 --- [rses-schedule-1] c.example.async.timetask.TestScheduling  : ThreadName:====one====courses-schedule-1
2019-11-13 17:24:22.022  INFO 5448 --- [rses-schedule-2] c.example.async.timetask.TestScheduling  : ThreadName:====two====courses-schedule-2
2019-11-13 17:24:22.022  INFO 5448 --- [rses-schedule-3] c.example.async.timetask.TestScheduling  : ThreadName:====one====courses-schedule-3

调用使用@Async注解的方法

这个主要还是自己基础太差的原因,在调用有@Async的方法的时候,注意调用该方法的方式,有些方式并不会使@Async注解产生作用。

比如以下情况:


@Component
public class AsyncTest {
    @Async
    public void testOne(){
        System.out.println("ThreadName:===one===="+Thread.currentThread().getName());
    }
}

@Service
public class AsyncServiceImpl implements AsyncService {
    @Autowired
    private AsyncTest asyncTest;
    @Override
    public void testAsync() {
        // 步骤一
        asyncTest.testOne();
        // 步骤二
        testOne();
    }
    @Async
    public void testOne() {
        System.out.println("ThreadName:===AsyncServiceImpl+testOne====" + Thread.currentThread().getName());
    }

}

这里注意步骤一和二,两个方法都是用了@Async注解的,但是步骤二就是方法调用,并不是异步调用;

总结

经验是不断积累的,需要学习的地方还有很多,继续努力;

参考:

以上是关于SpringBoot @Scheduled多线程执行的主要内容,如果未能解决你的问题,请参考以下文章

springboot 基于@Scheduled注解 实现定时任务

springboot中@Scheduled 和@Async的使用

spring boot 学习定时任务 @Scheduled

玩转SpringBoot之定时任务@Scheduled线程池配置-

SpringBoot多线程并发定时任务

springboot + @scheduled 多任务并发