Java | Master Timed Tasks in One Minute | 5 - Spring Task

Author: Mars Sauce

Disclaimer: This article was originally created by Mars sauce, and part of the content comes from the Internet. If you have any questions, please contact me.

Reprint: Welcome to reprint, please contact me before reprinting!

foreword

Multi-threading solves the problem of concurrent blocking, but it cannot express our timing method conveniently. At present, the annotation method in Spring Task is the most commonly used timing task in the single architecture?

@Scheduled

Several commonly used scheduled annotations:

cron: Support flexible cron expressions

fixedRate: fixed frequency. For example: the subway line 2 runs every 5 minutes, so all trains on line 2 have already arranged a timetable, so each train can be dispatched on time, but if one train is late, the next train will be delayed.

fixedDelay: Fixed delay. It means the time interval between the end of the previous task and the start of the next task. No matter how much time a task takes to execute, the interval between two tasks is always the same.

fuck it

@Scheduled(fixedDelay = 1000 * 5)
public void timerTaskA(){
    // Mars酱 做业务a...
}
复制代码

Execute every 5 seconds

@Scheduled(cron = "0 0 1 * * ? ")
public void timerTaskB(){
    // Mars酱 做业务b...
}
复制代码

This supports cron expressions and is executed at 1 am every day

Will Scheduled block?

Let's analyze the source code of Spring. If we use fixedRateor , we can find the following code fixedDelayin the implementation part of Spring's source code:@Scheduled

An object corresponding to the annotation will be added to a registrar object, which is a ScheduledTaskRegistrar object:

private final ScheduledTaskRegistrar registrar = new ScheduledTaskRegistrar();
复制代码

And this ScheduledTaskRegistrar object has a ScheduledExecutorService property:

	@Nullable
	private ScheduledExecutorService localExecutor;
复制代码

This is the implementation of the multi-threaded timing task we mentioned in the previous article. Continue to find the method of creating this object in the 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注解做定时任务已经支持得很完美了,满足大部分单体架构的定时任务需要。到站下车,下一站见了~

Guess you like

Origin juejin.im/post/7230222912725434427