作者:Mars酱
声明:本文章由Mars酱原创,部分内容来源于网络,如有疑问请联系本人。
转载:欢迎转载,转载前先请联系我!
前言
多线程解决了并发阻塞问题,但是不能方便的表达我们的定时方式,目前单体架构定时任务用的多的就应该是Spring Task中的注解方式了吧?
@Scheduled
scheduled注解常用的几个:
cron
:支持灵活的cron表达式
fixedRate
:固定频率。比如:2号线地铁每5分钟一趟,那么2号线的所有列车其实已经安排好了时刻表,所以每台准点发就行了,但是如果其中一趟晚点,那么下一趟就会延迟。
fixedDelay
:固定时延。它的意思是表示上个任务结束,到下个任务开始的时间间隔。无论任务执行花费多少时间,两个任务间的间隔始终是一致的。
搞一搞
@Scheduled(fixedDelay = 1000 * 5)
public void timerTaskA(){
// Mars酱 做业务a...
}
每间隔5秒执行一次
@Scheduled(cron = "0 0 1 * * ? ")
public void timerTaskB(){
// Mars酱 做业务b...
}
这是支持cron表达式的,每天凌晨1点执行
Scheduled会阻塞吗?
我们来分析下Spring的源代码吧,如果我们用fixedRate
或者fixedDelay
,可以在 Spring 的@Scheduled
的源代码实现部分找到如下代码:
会在一个registrar对象中添加注解相对应的对象,这个registrar是ScheduledTaskRegistrar对象:
private final ScheduledTaskRegistrar registrar = new ScheduledTaskRegistrar();
而这个ScheduledTaskRegistrar对象中有一个ScheduledExecutorService属性:
@Nullable
private ScheduledExecutorService localExecutor;
这个就是我们上篇中提到的多线程定时任务实现,继续在在ScheduledTaskRegistrar中又找到创建这个对象的方法:
/**
* Schedule all registered tasks against the underlying
* {@linkplain #setTaskScheduler(TaskScheduler) task scheduler}.
*/
@SuppressWarnings("deprecation")
protected void scheduleTasks() {
if (this.taskScheduler == null) {
// 创建了一个单线程对象
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
if (this.triggerTasks != null) {
for (TriggerTask task : this.triggerTasks) {
addScheduledTask(scheduleTriggerTask(task));
}
}
if (this.cronTasks != null) {
for (CronTask task : this.cronTasks) {
addScheduledTask(scheduleCronTask(task));
}
}
if (this.fixedRateTasks != null) {
for (IntervalTask task : this.fixedRateTasks) {
addScheduledTask(scheduleFixedRateTask(task));
}
}
if (this.fixedDelayTasks != null) {
for (IntervalTask task : this.fixedDelayTasks) {
addScheduledTask(scheduleFixedDelayTask(task));
}
}
}
这里第一个if判断就是创建那个localExecutor对象,使用的是newSingleThreadScheduledExecutor。在 Java | 一分钟掌握异步编程 | 3 - 线程异步 - 掘金 (juejin.cn)中提到过,这是创建单线程的线程池方式。那么一个单线程去跑多个定时任务是不是就会产生阻塞?来证明一下。
改写一下之前的例子,两个都是5秒执行,其中一个任务在执行的时候再延迟10秒,看是不是会影响到另一个线程的定时任务执行。改写后的代码如下:
import org.springframework.boot.SpringApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import java.util.Date;
/**
* @author mars酱
*/
@EnableScheduling
public class MarsSpringScheduled {
public static void main(String[] args) {
SpringApplication.run(MarsSpringScheduled.class, args);
}
@Scheduled(fixedDelay = 5000)
public void timerTaskA() throws InterruptedException {
System.out.println(">> 这是a任务:当前时间:" + new Date());
Thread.sleep(10000);
}
@Scheduled(fixedDelay = 5000)
public void timerTaskB() {
System.out.println("<< 这是b任务:当前毫秒:" + System.currentTimeMillis());
}
}
运行一下,Mars酱得到结果如下:
可以看到a任务的输出延迟了15秒,b任务是毫秒数,拿后一个毫秒数减去前一个毫秒数,中间相差也几乎是15秒,看来是被阻塞了啊
怎么解决 @Scheduled 的阻塞?
既然依赖方式是ScheduledExecutorService被ScheduledTaskRegistrar包含,ScheduledTaskRegistrar又是在Spring的后置处理器中使用的,那么我们无法修改Spring的注解后置处理器,只能修改ScheduledTaskRegistrar了,在Spring代码中找到设置这个的部分,代码如下:
private void finishRegistration() {
if (this.scheduler != null) {
this.registrar.setScheduler(this.scheduler);
}
if (this.beanFactory instanceof ListableBeanFactory) {
Map<String, SchedulingConfigurer> beans =
((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);
List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values());
AnnotationAwareOrderComparator.sort(configurers);
for (SchedulingConfigurer configurer : configurers) {
// 配置ScheduledTaskRegistrar对象
configurer.configureTasks(this.registrar);
}
}
...
}
在configurer中配置了ScheduledTaskRegistrar对象啊~。SchedulingConfigurer在Spring源代码中查找到是个接口:
@FunctionalInterface
public interface SchedulingConfigurer {
/**
* Callback allowing a {@link org.springframework.scheduling.TaskScheduler
* TaskScheduler} and specific {@link org.springframework.scheduling.config.Task Task}
* instances to be registered against the given the {@link ScheduledTaskRegistrar}
* @param taskRegistrar the registrar to be configured.
*/
void configureTasks(ScheduledTaskRegistrar taskRegistrar);
}
那么我们只要实现这个接口,改变ScheduledTaskRegistrar中ScheduledExecutorService线程池的大小不就可以了?改一下吧:
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
}
}
修改线程是个一个固定大小的线程池,大小为10,再拆分原来的两个定时任务为单独的对象:
/**
* (这个类的说明)
*
* @author mars酱
*/
@Service
public class TimerTaskA {
@Scheduled(fixedDelay = 5000)
public void scheduler() throws InterruptedException {
System.out.println(">> 这是a任务:当前时间:" + new Date());
Thread.sleep(10000);
}
}
上面是任务A,下面是任务B,一上一下其乐融融:
/**
* (这个类的说明)
*
* @author mars酱
*/
@Service
public class TimerTaskB {
@Scheduled(fixedDelay = 2000)
public void scheduler() {
System.out.println("<< 这是b任务:当前时间:" + new Date());
}
}
再修改启动函数:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @author mars酱
*/
@EnableScheduling
@SpringBootApplication(scanBasePackages = {"com.mars.time"})
public class MarsSpringScheduled {
public static void main(String[] args) {
SpringApplication.run(MarsSpringScheduled.class, args);
}
// @Async
// @Scheduled(fixedDelay = 5000)
// public void timerTaskA() throws InterruptedException {
// System.out.println(">> 这是a任务:当前时间:" + new Date());
// Thread.sleep(10000);
// }
//
@Async
// @Scheduled(fixedDelay = 2000)
// public void timerTaskB() {
// System.out.println("<< 这是b任务:当前时间:" + new Date());
// }
}
运行一下,得到结果:
可以看到任务b保证每2秒执行一次,a任务按照自己的频率在执行,各自不影响了。我们设置ScheduledTaskRegistrar中线程池大小是成功的。
为什么要拆?
如果不拆成两个,就算加大Spring定时任务内的线程池大小,也没有用。因为一个对象中包含两个定时任务函数,那个对象在Spring的定时任务框架内是一个对象。
那是不是拆成两个对象,就不会相互影响了呢?也不是,因为默认线程池是单线程,拆成了两个也会阻塞,所以需要加大线程池,而且还要拆成两个对象,这样才解决定时任务的阻塞情况。
可以试试把自定义的ScheduleConfig去掉,然后再启动,得到的结果依然会是阻塞的。
总结
Spring Scheduled注解做定时任务已经支持得很完美了,满足大部分单体架构的定时任务需要。到站下车,下一站见了~