作者: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的源代码实现部分找到如下代码:

Java | 一分钟掌握定时任务 | 5 - Spring Task_定时任务

会在一个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(">> 这是mars酱 a任务:当前时间:" + new Date());
        Thread.sleep(10000);
    }

    @Scheduled(fixedDelay = 5000)
    public void timerTaskB() {
        System.out.println("<< 这是mars酱 b任务:当前毫秒:" + System.currentTimeMillis());
    }
}

运行一下,结果如下:

Java | 一分钟掌握定时任务 | 5 - Spring Task_定时任务_02

可以看到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(">> 这是mars酱 a任务:当前时间:" + new Date());
        Thread.sleep(10000);
    }
}

上面是任务A,下面是任务B,一上一下其乐融融:

/**
 * (这个类的说明)
 *
 * @author mars酱
 */

@Service
public class TimerTaskB {
    @Scheduled(fixedDelay = 2000)
    public void scheduler() {
        System.out.println("<< 这是mars酱 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());
//    }
}

运行一下,mars酱 得到结果:

Java | 一分钟掌握定时任务 | 5 - Spring Task_定时任务_03

可以看到任务b保证每2秒执行一次,a任务按照自己的频率在执行,各自不影响了。mars酱 设置ScheduledTaskRegistrar中线程池大小是成功的。

为什么要拆?

如果不拆成两个,就算加大Spring定时任务内的线程池大小,也没有用。因为一个对象中包含两个定时任务函数,那个对象在Spring的定时任务框架内是一个对象。

那是不是拆成两个对象,就不会相互影响了呢?也不是,因为默认线程池是单线程,拆成了两个也会阻塞,所以需要加大线程池,而且还要拆成两个对象,这样才解决定时任务的阻塞情况。

可以试试把自定义的ScheduleConfig去掉,然后再启动,得到的结果依然会是阻塞的。

总结

Spring Scheduled注解做定时任务已经支持得很完美了,满足大部分单体架构的定时任务需要。到站下车,下一站见了~