Spring的@Scheduled任务调度

一. 定时任务实现方式

1.1 定时任务实现方式

  • Java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行。一般用的较少,这篇文章将不做详细介绍。
  • 使用Quartz,这是一个功能比较强大的的调度器,可以让你的程序在指定时间执行,也可以按照某一个频度执行,配置起来稍显复杂,有空介绍。
  • 使用Spring的@Scheduled注解配合@EnableScheduling一起使用,本文主要介绍。

说明:@Scheduled 注解用于标注这个方法是一个定时任务的方法,有多种配置可选。cron支持cron表达式,指定任务在特定时间执行;fixedRate以特定频率执行任务;fixedRateString以string的形式配置执行频率。

1.2 定时任务执行方式

  • 单线程(串行)。多个定时任务时串行执行的;如果一个任务出现阻塞,其他的任务都会受到影响;顺序是串掉的,并没有表现出明显的优先级关系
  • 多线程(并行)

二. 创建定时任务

2.1、Spring 中串行调度

在Spring 中自带了 @Scheduled,实现起来很方便,只需要在需要调度的方法上增加注解即可。

 下面分别介绍 cron、fixedDelay、fixedRate 三种风湿。

先在启动类上添加 @EnableScheduling 注解开始支持。

package com.johnfnash.learn.springboot.schedule;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class ScheduleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ScheduleApplication.class, args);
    }
}

2.1.1 cron 表达式

使用cron表达式标注任务方法。下面的表示每分钟的0、5、10、15 … 秒执行任务。

package com.johnfnash.learn.springboot.schedule.task;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class TestTask {
     @Scheduled(cron = "0/5 * * * * ?")
     public void doTask() throws InterruptedException {
         log.info(Thread.currentThread().getName()+"===task run");         
         log.info(Thread.currentThread().getName()+"===task end");
     }
}

启动项目,运行结果如下:

2019-06-02 11:42:35.007  INFO 9740 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task run
2019-06-02 11:42:35.007  INFO 9740 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task end
2019-06-02 11:42:40.003  INFO 9740 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task run
2019-06-02 11:42:40.003  INFO 9740 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task end
2019-06-02 11:42:45.001  INFO 9740 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task run
2019-06-02 11:42:45.001  INFO 9740 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task end

可以看到,定时任务的确是按照配置执行的。

如果任务没有在5秒内执行完成,spring 会怎么处理呢?

修改上面的方法,模拟任务执行时间超过任务执行间隔。

@Scheduled(cron = "0/5 * * * * ?")
public void doTask() throws InterruptedException {
 log.info(Thread.currentThread().getName()+"===task run");         
 Thread.sleep(6000);
 log.info(Thread.currentThread().getName()+"===task end");
}

结果日志:

2019-06-02 13:13:25.009  INFO 11012 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task run
2019-06-02 13:13:31.020  INFO 11012 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task end
2019-06-02 13:13:35.001  INFO 11012 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task run
2019-06-02 13:13:41.014  INFO 11012 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task end
2019-06-02 13:13:45.014  INFO 11012 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task run
2019-06-02 13:13:51.018  INFO 11012 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task end

       我们可以发现,spring他的处理方式是等待上一个任务执行完成后,再去执行下一个任务,也就是说spring的定时任务默认是单线程的,而不会开启另一条线程去执行任务。

       spring在处理使用cron表达式这种定时任务时,依旧关注的是任务的开始时间,但是他和fixedDelay不同的是,他会在配置任务开始时判断任务是否可以执行,如果可以则执行,如果不可以,那么他将不执行此次任务,等待下一次执行

2.1.2 fixedDelay

测试代码:

@Scheduled(initialDelay = 3000, fixedDelay = 5000)
public void doTask2() throws InterruptedException {
     log.info(Thread.currentThread().getName()+"===task2 run");
     Thread.sleep(6000);
     log.info(Thread.currentThread().getName()+"===task2 end");
}

结果日志:

2019-06-02 13:19:22.079  INFO 10648 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task2 run
2019-06-02 13:19:28.079  INFO 10648 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task2 end
2019-06-02 13:19:33.089  INFO 10648 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task2 run
2019-06-02 13:19:39.098  INFO 10648 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task2 end
2019-06-02 13:19:44.109  INFO 10648 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task2 run
2019-06-02 13:19:50.109  INFO 10648 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task2 end

       我们可以发现,这个结果和上一个使用cro表达式的结果好像啊。但是仔细看我们就会发现,这次所有的时间都是我们配置的:任务开始到结束间隔6s,上一个任务结束时间下一个任务开始时间是5秒,这样看来是比较符合我们的设置的。

       fixedDelay是设定上一个任务结束后多久执行下一个任务,也就是fixedDelay只关心上一任务的结束时间和下一任务的开始时间

2.1.3 fixedRate

这次我们先将6s的任务执行时间去掉,看看输出什么

测试代码:

@Scheduled(initialDelay = 3000, fixedRate = 5000)
public void doTask3() throws InterruptedException {
    log.info(Thread.currentThread().getName()+"===task3 run");
    //Thread.sleep(6000);
    log.info(Thread.currentThread().getName()+"===task3 end");
}

结果日志:

2019-06-02 13:23:10.781  INFO 12248 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 run
2019-06-02 13:23:10.781  INFO 12248 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 end
2019-06-02 13:23:15.781  INFO 12248 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 run
2019-06-02 13:23:15.781  INFO 12248 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 end
2019-06-02 13:23:20.771  INFO 12248 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 run
2019-06-02 13:23:20.771  INFO 12248 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 end
2019-06-02 13:23:25.782  INFO 12248 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 run
2019-06-02 13:23:25.782  INFO 12248 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 end

       诶?这样看来好像和其他两个没什么区别,都是间隔5s执行方法啊。别急,现在我们把注掉的6s任务执行时间放开,再来看看结果

2019-06-02 13:24:19.981  INFO 11972 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 run
2019-06-02 13:24:26.001  INFO 11972 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 end
2019-06-02 13:24:26.001  INFO 11972 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 run
2019-06-02 13:24:32.021  INFO 11972 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 end
2019-06-02 13:24:32.021  INFO 11972 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 run
2019-06-02 13:24:38.031  INFO 11972 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 end

       这次我们可以看到,上一个任务结束后,下一个任务立刻开始执行了,结合第一次测试,我们就可以推断,fixedRate设置的上一个任务的开始时间到下一个任务开始时间的间隔,那我们的推断对不对呢?这次我们把任务执行时间改成2s,测试走起~

2019-06-02 13:25:44.008  INFO 4872 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 run
2019-06-02 13:25:46.019  INFO 4872 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 end
2019-06-02 13:25:48.998  INFO 4872 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 run
2019-06-02 13:25:51.008  INFO 4872 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 end
2019-06-02 13:25:54.008  INFO 4872 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 run
2019-06-02 13:25:56.018  INFO 4872 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 end
2019-06-02 13:25:58.998  INFO 4872 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 run
2019-06-02 13:26:01.007  INFO 4872 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task3 end

       结果和我们推断的一致,两个任务的开始时间间隔是5s,当到达任务的开始执行时间,但上一个任务却没有完成时,spring会等待上一个任务执行完,并立即开始执行本次任务

2.2、Spring 中并行调度

       要在 Spring 中实现并行调度,可以新建一个配置类来实现 SchedulingConfigurer 接口,也可以使用 @Async 注解。下面分别进行介绍。

2.2.1 实现 SchedulingConfigurer

     添加如下配置类:

package com.johnfnash.learn.springboot.schedule;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

@Configuration
@EnableScheduling
public class ScheduleConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(taskExecutor());
    }

    @Bean(destroyMethod = "shutdown")
    public Executor taskExecutor() {
        return Executors.newScheduledThreadPool(10);
    }
}

测试代码:

package com.johnfnash.learn.springboot.schedule.task;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class TestTask {
    @Scheduled(cron = "0/5 * * * * ?")
    public void doTask() throws InterruptedException {
        log.info(Thread.currentThread().getName()+"===task run");
        log.info(Thread.currentThread().getName()+"===task end");
    }
    
    @Async
    @Scheduled(initialDelay = 3000, fixedDelay = 5000)
    public void doTask2() throws InterruptedException {
        log.info(Thread.currentThread().getName()+"===task2 run");
        log.info(Thread.currentThread().getName()+"===task2 end");
    }
    
    @Scheduled(initialDelay = 3000, fixedRate = 5000)
    public void doTask3() throws InterruptedException {
        log.info(Thread.currentThread().getName()+"===task3 run");
        log.info(Thread.currentThread().getName()+"===task3 end");
    }
}

结果日志:

2019-06-02 13:50:50.012  INFO 5960 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task run
2019-06-02 13:50:50.012  INFO 5960 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task end
2019-06-02 13:50:50.052  INFO 5960 --- [pool-1-thread-2] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-2===task3 run
2019-06-02 13:50:50.052  INFO 5960 --- [pool-1-thread-3] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-3===task2 run
2019-06-02 13:50:50.052  INFO 5960 --- [pool-1-thread-2] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-2===task3 end
2019-06-02 13:50:50.052  INFO 5960 --- [pool-1-thread-3] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-3===task2 end
2019-06-02 13:50:55.013  INFO 5960 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task run
2019-06-02 13:50:55.013  INFO 5960 --- [pool-1-thread-1] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-1===task end
2019-06-02 13:50:55.052  INFO 5960 --- [pool-1-thread-4] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-4===task3 run
2019-06-02 13:50:55.052  INFO 5960 --- [pool-1-thread-4] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-4===task3 end
2019-06-02 13:50:55.072  INFO 5960 --- [pool-1-thread-2] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-2===task2 run
2019-06-02 13:50:55.072  INFO 5960 --- [pool-1-thread-2] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-2===task2 end
2019-06-02 13:51:00.002  INFO 5960 --- [pool-1-thread-3] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-3===task run
2019-06-02 13:51:00.002  INFO 5960 --- [pool-1-thread-3] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-3===task end
2019-06-02 13:51:00.042  INFO 5960 --- [pool-1-thread-5] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-5===task3 run
2019-06-02 13:51:00.042  INFO 5960 --- [pool-1-thread-5] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-5===task3 end
2019-06-02 13:51:00.082  INFO 5960 --- [pool-1-thread-6] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-6===task2 run
2019-06-02 13:51:00.082  INFO 5960 --- [pool-1-thread-6] c.j.l.springboot.schedule.task.TestTask  : pool-1-thread-6===task2 end

2.2.2 使用 @Async

启动类添加 @EnableAsync 开启异步方法支持。

@SpringBootApplication
@EnableScheduling
@EnableAsync
public class ScheduleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ScheduleApplication.class, args);
    }
}

添加 @Async 注解。

@Async
@Scheduled(initialDelay = 3000, fixedDelay = 5000)
public void doTask2() throws InterruptedException {
    log.info(Thread.currentThread().getName()+"===task2 run");
//      Thread.sleep(6000);
    log.info(Thread.currentThread().getName()+"===task2 end");
}

@Async
@Scheduled(initialDelay = 3000, fixedRate = 5000)
public void doTask3() throws InterruptedException {
    log.info(Thread.currentThread().getName()+"===task3 run");
//      Thread.sleep(2000);
    log.info(Thread.currentThread().getName()+"===task3 end");
}

结果日志:

2019-06-02 13:58:28.434  INFO 4600 --- [cTaskExecutor-1] c.j.l.springboot.schedule.task.TestTask  : SimpleAsyncTaskExecutor-1===task3 run
2019-06-02 13:58:28.434  INFO 4600 --- [cTaskExecutor-2] c.j.l.springboot.schedule.task.TestTask  : SimpleAsyncTaskExecutor-2===task2 run
2019-06-02 13:58:28.434  INFO 4600 --- [cTaskExecutor-1] c.j.l.springboot.schedule.task.TestTask  : SimpleAsyncTaskExecutor-1===task3 end
2019-06-02 13:58:28.434  INFO 4600 --- [cTaskExecutor-2] c.j.l.springboot.schedule.task.TestTask  : SimpleAsyncTaskExecutor-2===task2 end
2019-06-02 13:58:33.424  INFO 4600 --- [cTaskExecutor-3] c.j.l.springboot.schedule.task.TestTask  : SimpleAsyncTaskExecutor-3===task3 run
2019-06-02 13:58:33.424  INFO 4600 --- [cTaskExecutor-3] c.j.l.springboot.schedule.task.TestTask  : SimpleAsyncTaskExecutor-3===task3 end
2019-06-02 13:58:33.429  INFO 4600 --- [cTaskExecutor-4] c.j.l.springboot.schedule.task.TestTask  : SimpleAsyncTaskExecutor-4===task2 run
2019-06-02 13:58:33.429  INFO 4600 --- [cTaskExecutor-4] c.j.l.springboot.schedule.task.TestTask  : SimpleAsyncTaskExecutor-4===task2 end
......
2019-06-02 13:59:03.437  INFO 4600 --- [TaskExecutor-16] c.j.l.springboot.schedule.task.TestTask  : SimpleAsyncTaskExecutor-16===task2 run
2019-06-02 13:59:03.438  INFO 4600 --- [TaskExecutor-16] c.j.l.springboot.schedule.task.TestTask  : SimpleAsyncTaskExecutor-16===task2 end
2019-06-02 13:59:08.424  INFO 4600 --- [TaskExecutor-17] c.j.l.springboot.schedule.task.TestTask  : SimpleAsyncTaskExecutor-17===task3 run
2019-06-02 13:59:08.424  INFO 4600 --- [TaskExecutor-17] c.j.l.springboot.schedule.task.TestTask  : SimpleAsyncTaskExecutor-17===task3 end
2019-06-02 13:59:08.439  INFO 4600 --- [TaskExecutor-18] c.j.l.springboot.schedule.task.TestTask  : SimpleAsyncTaskExecutor-18===task2 run
2019-06-02 13:59:08.439  INFO 4600 --- [TaskExecutor-18] c.j.l.springboot.schedule.task.TestTask  : SimpleAsyncTaskExecutor-18===task2 end
......

    使用默认的 @Async 配置,线程池开启的线程挺多的。我们可以通过实现 AsyncConfigurer 接口来进行灵活的设置。

package com.johnfnash.learn.springboot.schedule;

import java.lang.reflect.Method;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.StringUtils;

import lombok.extern.slf4j.Slf4j;

@Configuration
@Slf4j
public class AsyncConfig implements AsyncConfigurer {

    // 自定义线程池
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(100);
        // 设置线程名称前缀(前缀+线程序号作为最终的线程名称)
        //executor.setThreadNamePrefix("AsyncExecT-");
        executor.setThreadFactory(new MyNamedThreadFactory("Async"));
        // 设置拒绝策略
        //executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        executor.initialize(); //如果不初始化,导致找到不到执行器
        //ExecutorService executor = Executors.newFixedThreadPool(10, new MyNamedThreadFactory("PHS"));
        return executor;
    }
    
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new MyAsyncExceptionHandler();
    }
    
    // 自定义异常处理类
    class MyAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

        @Override
        public void handleUncaughtException(Throwable throwable, Method method, Object... params) {
            log.info("Exception message - " + throwable.getMessage());
            log.info("Method name - " + method.getName());
        }
        
    }

    // 自定义 ThreadFactory
    static class MyNamedThreadFactory implements ThreadFactory {

        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private String namePrefix;
        
        public MyNamedThreadFactory(String name) {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
            if(StringUtils.isEmpty(name)) {
                name = "pool";
            }
            
            namePrefix = name + "-" + poolNumber.getAndIncrement() + "-thread";
        }
        
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
            if(t.isDaemon()) {
                t.setDaemon(true);
            }
            if(t.getPriority() != Thread.NORM_PRIORITY) {
                t.setPriority(Thread.NORM_PRIORITY);
            }
            return t;
        }
        
    }
    
}

上面的配置类中设置了线程池的一些属性、异常信息处理类、ThreadFactory 等。

结果日志:

2019-06-02 14:02:37.187  INFO 12504 --- [Async-1-thread1] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread1===task3 run
2019-06-02 14:02:37.187  INFO 12504 --- [Async-1-thread2] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread2===task2 run
2019-06-02 14:02:37.187  INFO 12504 --- [Async-1-thread1] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread1===task3 end
2019-06-02 14:02:37.187  INFO 12504 --- [Async-1-thread2] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread2===task2 end
2019-06-02 14:02:42.177  INFO 12504 --- [Async-1-thread3] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread3===task3 run
2019-06-02 14:02:42.178  INFO 12504 --- [Async-1-thread3] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread3===task3 end
2019-06-02 14:02:42.182  INFO 12504 --- [Async-1-thread4] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread4===task2 run
2019-06-02 14:02:42.182  INFO 12504 --- [Async-1-thread4] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread4===task2 end
2019-06-02 14:02:47.178  INFO 12504 --- [Async-1-thread5] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread5===task3 run
2019-06-02 14:02:47.179  INFO 12504 --- [Async-1-thread5] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread5===task3 end
2019-06-02 14:02:47.184  INFO 12504 --- [Async-1-thread6] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread6===task2 run
2019-06-02 14:02:47.185  INFO 12504 --- [Async-1-thread6] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread6===task2 end
2019-06-02 14:02:52.176  INFO 12504 --- [Async-1-thread7] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread7===task3 run
2019-06-02 14:02:52.177  INFO 12504 --- [Async-1-thread7] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread7===task3 end
2019-06-02 14:02:52.184  INFO 12504 --- [Async-1-thread8] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread8===task2 run
2019-06-02 14:02:52.184  INFO 12504 --- [Async-1-thread8] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread8===task2 end
2019-06-02 14:02:57.178  INFO 12504 --- [Async-1-thread9] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread9===task3 run
2019-06-02 14:02:57.178  INFO 12504 --- [Async-1-thread9] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread9===task3 end
2019-06-02 14:02:57.185  INFO 12504 --- [sync-1-thread10] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread10===task2 run
2019-06-02 14:02:57.185  INFO 12504 --- [sync-1-thread10] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread10===task2 end
2019-06-02 14:03:02.177  INFO 12504 --- [Async-1-thread1] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread1===task3 run
2019-06-02 14:03:02.178  INFO 12504 --- [Async-1-thread1] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread1===task3 end
2019-06-02 14:03:02.187  INFO 12504 --- [Async-1-thread2] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread2===task2 run
2019-06-02 14:03:02.187  INFO 12504 --- [Async-1-thread2] c.j.l.springboot.schedule.task.TestTask  : Async-1-thread2===task2 end

简单说一下,用自定义线程池的好处:

  • 合理的分配线程池参数
  • 拒绝策略的选择也比较有意思(可以按照自己的想法来处理"负载"的任务)
  • 线程池命名,对于以后问题排查,会有很大的帮助

3. @Scheduled参数说明

  • initial-delay : 表示第一次运行前需要延迟的时间,单位是毫秒
  • fixed-delay : 表示从上一个任务完成到下一个任务开始的间隔, 单位是毫秒。
  • fixed-rate : 表示从上一个任务开始到下一个任务开始的间隔, 单位是毫秒。(如果上一个任务执行超时,则可能是上一个任务执行完成后立即启动下一个任务)
  • cron : cron 表达式。(定时执行,如果上一次任务执行超时而导致某个定时间隔不能执行,则会顺延下一个定时间隔时间。下一个任务和上一个任务的间隔时间不固定)

区别见图:

schedule

局限性——@Scheduled的cron无法指定执行的年份

即我们假如使用下面的定时任务

@Scheduled(cron = "0 18 10 * * ? 2016-2016")
public void testTaskWithDate() {
    logger.info("测试2016.定时任务");
}

将会报下面的错误

Cron expression must consist of 6 fields (found 7 in "0 18 10 * * ? 2016-2016")
Caused by: java.lang.IllegalStateException: Encountered invalid @Scheduled method 'testTaskWithDate': Cron expression must consist of 6 fields (found 7 in "0 18 10 * * ? 2016-2016")
    at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.processScheduled(ScheduledAnnotationBeanPostProcessor.java:405)
    at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.postProcessAfterInitialization(ScheduledAnnotationBeanPostProcessor.java:258)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:422)

4. Cron表达式的详细用法

字段 允许值 允许的特殊字符
0-59 - * /
0-59 - * /
小时 0-23 - * /
日期 1-31 - * ? / L W C
月份 1-12 或者 JAN-DEC - * /
星期 1-7 或者 SUN-SAT - * ? / L C #
(可选) 留空, 1970-2099 - * /

例子:

0/5 * * * * ? : 每5秒执行一次

  • ”字符被用来指定所有的值。如:""在分钟的字段域里表示“每分钟”。
  • “?”字符只在日期域和星期域中使用。它被用来指定“非明确的值”。当你需要通过在这两个域中的一个来指定一些东西的时候,它是有用的。看下面的例子你就会明白。月份中的日期和星期中的日期这两个元素时互斥的一起应该通过设置一个问号来表明不想设置那个字段。
  • “-”字符被用来指定一个范围。如:“10-12”在小时域意味着“10点、11点、12点”。
  • “,”字符被用来指定另外的值。如:“MON,WED,FRI”在星期域里表示”星期一、星期三、星期五”。
  • “/”字符用于指定增量。如:“0/15”在秒域意思是每分钟的0,15,30和45秒。“5/15”在分钟域表示每小时的5,20,35和50。 符号“”在“/”前面(如:/10)等价于0在“/”前面(如:0/10)。记住一条本质:表达式的每个数值域都是一个有最大值和最小值的集合,如: 秒域和分钟域的集合是0-59,日期域是1-31,月份域是1-12。字符“/ ”可以帮助你在每个字符域中取相应的数值。如:“7/6”在月份域的时候只 有当7月的时候才会触发,并不是表示每个6月。
  • L是‘last’的省略写法可以表示day-of-month和day-of-week域,但在两个字段中的意思不同,例如day-of- month域中表示一个月的最后一天。如果在day-of-week域表示‘7’或者‘SAT’,如果在day-of-week域中前面加上数字,它表示 一个月的最后几天,例如‘6L’就表示一个月的最后一个星期五。
    字符“W”只允许日期域出现。这个字符用于指定日期的最近工作日。例如:如果你在日期域中写 “15W”,表示:这个月15号最近的工作日。所以,如果15号是周六,则任务会在14号触发。如果15好是周日,则任务会在周一也就是16号触发。如果 是在日期域填写“1W”即使1号是周六,那么任务也只会在下周一,也就是3号触发,“W”字符指定的最近工作日是不能够跨月份的。字符“W”只能配合一个 单独的数值使用,不能够是一个数字段,如:1-15W是错误的。“L”和“W”可以在日期域中联合使用,LW表示这个月最后一周的工作日。
  • 字符“#”只允许在星期域中出现。这个字符用于指定本月的某某天。例如:“6#3”表示本月第三周的星期五(6表示星期五,3表示第三周)。“2#1”表示本月第一周的星期一。“4#5”表示第五周的-星期三。
  • 字符“C”允许在日期域和星期域出现。这个字符依靠一个指定的“日历”。也就是说这个表达式的值依赖于相关的“日历”的计算结果,如果没有“日历” 关联,则等价于所有包含的“日历”。如:日期域是“5C”表示关联“日历”中第一天,或者这个月开始的第一天的后5天。星期域是“1C”表示关联“日历” 中第一天,或者星期的第一天的后1天,也就是周日的后一天(周一)。

5. 表达式举例

例子:

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分每分触发
0 0/5 14 * * ? 每天下午的 2点到2点59分(整点开始,每隔5分触发)
0 0/5 14,18 * * ? 每天下午的 2点到2点59分、18点到18点59分(整点开始,每隔5分触发)
0 0-5 14 * * ? 每天下午的 2点到2点05分每分触发
0 10,44 14 ? 3 WED 3月分每周三下午的 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 ? * 6L 2002-2005 从2002年到2005年每月最后一周的星期五的10点15分触发
0 15 10 ? * 6#3 每月的第三周的星期五开始触发
0 0 12 1/5 * ? 每月的第一个中午开始每隔5天触发一次
0 11 11 11 11 ? 每年的11月11号 11点11分触发(光棍节)

6. 总结

  • fixedRate配置了上一次任务的开始时间下一次任务的开始时间的间隔,每次任务都会执行
  • fixedDelay配置了上一次任务的结束时间下一次任务的开始时间的间隔,每次任务都会执行
  • cron表达式配置了在哪一刻执行任务,会在配置的任务开始时间判断任务是否可以执行,如果能则执行,不能则会跳过本次执行
  • 如果是强调任务间隔的定时任务,建议使用fixedRate和fixedDelay,如果是强调任务在某时某分某刻执行的定时任务,建议使用cron表达式

7. 本文转自

  1. Spring定时任务@Scheduled注解使用方式浅窥(cron表达式、fixedRate和fixedDelay)

  2. Spring定时任务高级使用篇

  3. Spring的@Scheduled任务调度

发布了120 篇原创文章 · 获赞 125 · 访问量 107万+

猜你喜欢

转载自blog.csdn.net/yangyangye/article/details/102678646
今日推荐