SpringBoot使用@scheduled定时执行任务的时候是在一个单线程中,如果有多个任务,其中一个任务执行时间过长,则有可能会导致其他后续任务被阻塞直到该任务执行完成。也就是会造成一些任务无法定时执行的错觉。

可以通过如下代码进行测试:

 @Scheduled(cron = "0/1 * * * * ? ")
    public void deleteFile() throws InterruptedException {
        log.info("111delete success, time:" + new Date().toString());
        Thread.sleep(1000 * 5);//模拟长时间执行,比如IO操作,http请求
    }
 
    @Scheduled(cron = "0/1 * * * * ? ")
    public void syncFile() {
        log.info("222sync success, time:" + new Date().toString());
    }

**输出如下:
[pool-1-thread-1] : 111delete success, time:Mon Nov 26 20:42:13 CST 2018
[pool-1-thread-1] : 222sync success, time:Mon Nov 26 20:42:18 CST 2018
[pool-1-thread-1] : 111delete success, time:Mon Nov 26 20:42:19 CST 2018
[pool-1-thread-1] : 222sync success, time:Mon Nov 26 20:42:24 CST 2018
[pool-1-thread-1] : 222sync success, time:Mon Nov 26 20:42:25 CST 2018
[pool-1-thread-1] : 111delete success, time:Mon Nov 26 20:42:25 CST 2018

上面的日志中可以明显的看到syncFile被阻塞了,直达deleteFile执行完它才执行了
而且从日志信息中也可以看出@Scheduled是使用了一个线程池中的一个单线程来执行所有任务的。
**/

/**如果把Thread.sleep(1000*5)注释了,输出如下:
[pool-1-thread-1]: 111delete success, time:Mon Nov 26 20:48:04 CST 2018
[pool-1-thread-1]: 222sync success, time:Mon Nov 26 20:48:04 CST 2018
[pool-1-thread-1]: 222sync success, time:Mon Nov 26 20:48:05 CST 2018
[pool-1-thread-1]: 111delete success, time:Mon Nov 26 20:48:05 CST 2018
[pool-1-thread-1]: 111delete success, time:Mon Nov 26 20:48:06 CST 2018
[pool-1-thread-1]: 222sync success, time:Mon Nov 26 20:48:06 CST 2018
这下正常了
**/

我估计是在定时任务的配置中设定了一个SingleThreadScheduledExecutor,查看源码,从ScheduledAnnotationBeanPostProcessor类开始一路找下去。果然,在ScheduledTaskRegistrar(定时任务注册类)中的ScheduleTasks中又这样一段判断:


if (this.taskScheduler == null) {
    this.localExecutor = Executors.newSingleThreadScheduledExecutor();
    this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);

这就说明如果taskScheduler为空,那么就给定时任务做了一个单线程的线程池,正好在这个类中还有一个设置taskScheduler的方法:


public void setScheduler(Object scheduler) {
    Assert.notNull(scheduler, "Scheduler object must not be null");
    if (scheduler instanceof TaskScheduler) {
        this.taskScheduler = (TaskScheduler) scheduler;
    }
    else if (scheduler instanceof ScheduledExecutorService) {
        this.taskScheduler = new ConcurrentTaskScheduler(((ScheduledExecutorService) scheduler));
    }
    else {
        throw new IllegalArgumentException("Unsupported scheduler type: " + scheduler.getClass());
    }
}

解决办法

1、扩大原定时任务线程池中的核心线程数


@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(Executors.newScheduledThreadPool(50));
    }
}

这个方法,在程序启动后,会逐步启动50个线程,放在线程池中。每个定时任务会占用1个线程。但是相同的定时任务,执行的时候,还是在同一个线程中。
例如,程序启动,每个定时任务占用一个线程。任务1开始执行,任务2也开始执行。如果任务1卡死了,那么下个周期,任务1还是处理卡死状态,任务2可以正常执行。也就是说,任务1某一次卡死了,不会影响其他线程,但是他自己本身这个定时任务会一直等待上一次任务执行完成!

2、把Scheduled配置成成多线程执行


@Configuration
@EnableAsync
public class ScheduleConfig {
 
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(50);
        return taskScheduler;
    }
}

@EnableScheduling
public class TaskFileScheduleService {
 
 
    @Async
    @Scheduled(cron="0 */1 * * * ?")
    public void task1(){
    .......
    }
    
    @Async
    @Scheduled(cron="0 */1 * * * ?")
    public void task2(){
    .......
    }

这种方法,每次定时任务启动的时候,都会创建一个单独的线程来处理。也就是说同一个定时任务也会启动多个线程处理。
例如:任务1和任务2一起处理,但是线程1卡死了,任务2是可以正常执行的。且下个周期,任务1还是会正常执行,不会因为上一次卡死了,影响任务1。
但是任务1中的卡死线程越来越多,会导致50个线程池占满,还是会影响到定时任务。
这时候,可能会几个月发生一次~到时候再重启就行了!

3、将@Scheduled注释的方法内部改成异步执行


//当然了,构建一个合理的线程池也是一个关键,否则提交的任务也会在自己构建的线程池中阻塞
    ExecutorService service = Executors.newFixedThreadPool(5);
 
    @Scheduled(cron = "0/1 * * * * ? ")
    public void deleteFile() {
        service.execute(() -> {
            log.info("111delete success, time:" + new Date().toString());
            try {
                Thread.sleep(1000 * 5);//改成异步执行后,就算你再耗时也不会印象到后续任务的定时调度了
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
 
    @Scheduled(cron = "0/1 * * * * ? ")
    public void syncFile() {
        service.execute(()->{
            log.info("222sync success, time:" + new Date().toString());
        });
    }

4、把Scheduled配置成成多线程执行(解决阿里扫描规约问题)
application.properties

#定时任务触发器
hs.scheduer.orderScheduer=0/1 * * * * ?
hs.scheduer.wmsScheduer=0/1 * * * * ?
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

/**
 * 多线程执行定时任务
 *
 * @author
 * @date
 */
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
    private Logger logger = LoggerFactory.getLogger(ScheduleConfig.class);

    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(2);
        return taskScheduler;
    }


    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {

        logger.info("定时任务配置2个线程。");

        taskRegistrar.setScheduler(taskScheduler());
    }

}

  @Scheduled(cron ="${hs.scheduer.orderScheduer}")
    public InnerGmmService.PutNoticeAutoUpdateSchedulerOutput putNoticeAutoUpdateScheduler() {
        InnerGmmService.PutNoticeAutoUpdateSchedulerOutput putNoticeAutoUpdateSchedulerOutput = new InnerGmmService.PutNoticeAutoUpdateSchedulerOutput();
        List<Notice> noticeListByDate = null;
        // 判断是否小于零点5分
        if (dualDAO.getCurrTime() < NUMBER_CURR_TIME) {
            noticeListByDate = noticeDAO.getNoticeListByDate(dualDAO.getIntervalDate(), dualDAO.getCurrDate());
            LOGGER.info("Notice id [ {} ] begin [ {} ]" + "" + noticeListByDate.toString() + "{}" + dualDAO.getIntervalDate() + "{}" + dualDAO.getCurrDate());
            putNoticeAutoUpdate(noticeListByDate, String.valueOf(dualDAO.getIntervalDate()));
            putNoticeAutoUpdate(noticeListByDate, String.valueOf(dualDAO.getCurrDate()));
        } else {
            noticeListByDate = noticeDAO.getNoticeListByDate(dualDAO.getCurrDate(), dualDAO.getCurrDate());
            LOGGER.info("Notice id [ {} ] begin [ {} ]" + noticeListByDate.toString() + "当前日期" + dualDAO.getCurrDate());
            putNoticeAutoUpdate(noticeListByDate, String.valueOf(dualDAO.getCurrDate()));
        }

        return putNoticeAutoUpdateSchedulerOutput;
    }
 @Scheduled(cron ="${hs.scheduer.orderScheduer}")
    private void putRtmcAutoUpdate(){
        putRtmcAutoUpdateScheduler();
    }
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐