SpringBoot整合定时任务搭配线程池实例应用
*springboot最基本得定时任务Scheduled注解执行任务都是单线程执行,且多个Scheduled注解下的定时任务也都是同一个线程执行的,尤其在多个Scheduled任务的时候,这样是很容易发生线程堵塞的,所以一般我们都会采用自定义线程池ThreadPoolTaskExecutor。
1.首先springboot启动器添加@EnableScheduling开启定时任务
@EnableScheduling
@SpringBootApplication
public class TestScheduledApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(this.getClass());
}
public static void main(String[] args) {
new SpringApplicationBuilder(TestScheduledApplication.class).web(true).run(args);
}
}
2.我们采用@Configuration注解配置线程池类。首先类上加上注解@EnableAsync开启异步支持。
然后定义多个@bean方法配置多个线程池,可以根据任务的业务场景配置不同核心线程,最大线程,任务队列,以及拒绝策略。
@Configuration
@EnableAsync
public class ExecutorConfig1 {
@Bean
public Executor executor1() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("test-schedule1-");
executor.setMaxPoolSize(20);
executor.setCorePoolSize(15);
executor.setQueueCapacity(0);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
@Bean
public Executor executor2() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("test-schedule2-");
executor.setMaxPoolSize(20);
executor.setCorePoolSize(15);
executor.setQueueCapacity(0);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
3.@Bean注解生成的对象默认名字和方法名称相同,我们在任务的方法添加Async注解来指定哪个线程池来执行任务。Scheduled注解配置执行时间
// 间隔1秒执行一次
@Async("executor1")
@Scheduled(cron = "0/1 * * * * ?")
public void method1() {
logger.info("——————————method1 start——————————");
logger.info("——————————method1 end——————————");
}
// 间隔1秒执行一次
@Scheduled(cron = "0/1 * * * * ?")
@Async("executor2")
public void method2() {
logger.info("——————————method2 start——————————");
logger.info("——————————method2 end——————————");
}
二、线程池ThreadPoolTaskExecutor 工作原理示意图
三、注意点:
1.当使用Async注解异步执行任务的时候,没有配置自己的线程池,会默认使用SimpleAsyncTaskExecutor。它并不是真正的线程池,不会重用线程,每次执行任务都会开启新的线程
2.Scheduled 和quartz 对比的话,quartz配置比较复杂,需要定义任务类,然后任务类注入到任务工厂容器当中,任务容器传入到执行器的工厂容器,且配置执行时间,最后将触发器注入到任务工厂容器中等待执行。quartz虽然配置复杂,但是支持可视化动态配置定时任务,和在数据库中实现持久化任务
3.关于线程池为什么采用ThreadPoolTaskExecutor线程池。
(1)首先java的线程池顶级接口是Executor。里面只有一个execute(Runnable command)方法;执行一个包含任务的线程类对象,这样目的其实就是将任务模型和执行器分开来解耦合。 而他下面有俩大接口,分别是jdk的JUC包下面的ExecutorService和spring包下面的TaskExecutor接口
(2)jdk自带的线程池在JUC包下面 ThreadPoolExecutor是最底层的线程池,也包含了设置最大线程核心线程,但是一般项目中没有采用spring的情况下才会使用。而spring包下面的ThreadPoolTaskExecutor线程池其实内部也是封装ThreadPoolExecutor和在其方法上进行了一定的增强,增加了任务队列
(3)(包括线程池工具类Executors也是通过静态工厂方法,调用ThreadPoolExecutor的带参数构造方法来创建的常用四大自线程池)
4.JDK默认提供的拒绝策略
(1)有如下几种:
AbortPolicy:直接抛出异常,默认策略
CallerRunsPolicy:用调用者所在的线程来执行任务
DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务
DiscardPolicy:直接丢弃任务
(2).ThreadPoolTaskExecutor extends ExecutorConfigurationSupport,父类中通过三元运算配置拒绝策略,默认AbortPolicy(当队列和最大线程池都满了之后直接抛出异常)
public void setRejectedExecutionHandler(RejectedExecutionHandler rejectedExecutionHandler) { this.rejectedExecutionHandler = (rejectedExecutionHandler != null ? rejectedExecutionHandler : new ThreadPoolExecutor.AbortPolicy()); }
(3)ThreadPoolExecutor线程池类中定义了四个静态拒绝执行处理器类,而ThreadPoolTaskExecutor 的父类也正是调用了当中的四种策略处理器来完成线程策略
public static class AbortPolicy implements RejectedExecutionHandler {
/**
* Creates an {@code AbortPolicy}.
*/
public AbortPolicy() { }
/**
* Always throws RejectedExecutionException.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
* @throws RejectedExecutionException always
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
四、关于四大线程池底层队列的分析
FixedThreadPool和SingleThreadExecutor:允许请求队列最大长度为Integer.MAX_VALUE,导致OOM;CachedThreadPool和ScheduledThreadPool:允许创建线程的最大数量为Integer.MAX_VALUE,导致OOM。
并发库中有个类是BlockingQueue,就是阻塞队列,这个类提供了两个方法,put()和take(),前者是将对象放到队列里,如果没有空闲节点了,就等待,后者是从头拿对象,如果没有对象了就等到有。FixedThreadPool和SingleThreadExecutor都是采用无界的LinkedBlockingQueue,LinkedBlockingQueue中引入了两把锁,takeLock和putLock,分别用于take和put操作,因为入队和出队用的不同的锁,所以这个队列可以同时入队和出兑。后俩者的队列是SynchronousQueue
线程池对队列的三种状态:无界队列、有界队列、同步移交。
无界队列:请求无限增加,队列也会无限增加,会导致资源耗尽。