likes
comments
collection
share

【踩坑日记】我的@Scheduled 怎么不运行了?

作者站长头像
站长
· 阅读数 6

背景

近期开发着定制项目,因为并发要求较低但业务繁杂,需要多个定时任务执行数据和业务。

然后呢就在那么一个光和日丽,微风徐徐的下午

【踩坑日记】我的@Scheduled 怎么不运行了?

它就那样不运行了

原因

怎么就不运行了呢?怎么就不运行了呢?怎么就不运行了呢?

【踩坑日记】我的@Scheduled 怎么不运行了?

我也不知道呀?

来回忆一下怎么创建一个定时任务

  1. 启动Spring的定时任务能力@EnableScheduling
  2. 在被Spring管理的类中,在要定时执行的方法上加上@Scheduled(xxx)

第一步是加上@EnableScheduling,这个注解干了什么呢

@EnableScheduling

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {

}

@EnableScheduling注解Import了SchedulingConfiguration类

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {

    @Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
       return new ScheduledAnnotationBeanPostProcessor();
    }

}

SchedulingConfiguration类创建了个一个ScheduledAnnotationBeanPostProcessor类型,名为org.springframework.context.annotation.internalScheduledAnnotationProcessor的Bean

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {

    @Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
       return new ScheduledAnnotationBeanPostProcessor();
    }

}
public abstract class TaskManagementConfigUtils {

    /**
     * The bean name of the internally managed Scheduled annotation processor.
     */
    public static final String SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME =
          "org.springframework.context.annotation.internalScheduledAnnotationProcessor";
      ……

来看看ScheduledAnnotationBeanPostProcessor的源码

public class ScheduledAnnotationBeanPostProcessor
       implements ScheduledTaskHolder, MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor,
       Ordered, EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware,
       SmartInitializingSingleton, ApplicationListener<ContextRefreshedEvent>, DisposableBean {
       ……

ScheduledAnnotationBeanPostProcessor实现了spring相关生命周期的处理器、容器以及事件处理器,他的注解上说明了这个处理器用于注册注解了@Scheduled的方法,来看看他的#postProcessAfterInitialization方法

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
    if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||
          bean instanceof ScheduledExecutorService) {
       // Ignore AOP infrastructure such as scoped proxies.
       return bean;
    }

    Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
    if (!this.nonAnnotatedClasses.contains(targetClass)) {
       Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
             (MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
                Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(
                      method, Scheduled.class, Schedules.class);
                return (!scheduledMethods.isEmpty() ? scheduledMethods : null);
             });
       if (annotatedMethods.isEmpty()) {
          this.nonAnnotatedClasses.add(targetClass);
          if (logger.isTraceEnabled()) {
             logger.trace("No @Scheduled annotations found on bean class: " + targetClass);
          }
       }
       else {
          // Non-empty set of methods
          annotatedMethods.forEach((method, scheduledMethods) ->
                scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
          if (logger.isTraceEnabled()) {
             logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
                   "': " + annotatedMethods);
          }
       }
    }
    return bean;
}

这个方法找到所有注解了@Scheduled的方法,然后遍历调用processScheduled,来看看他的处理方法#processScheduled

/**
 * Process the given {@code @Scheduled} method declaration on the given bean.
 * @param scheduled the @Scheduled annotation
 * @param method the method that the annotation has been declared on
 * @param bean the target bean instance
 * @see #createRunnable(Object, Method)
 */
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
    try {
       Runnable runnable = createRunnable(bean, method);
       boolean processedSchedule = false;
       String errorMessage =
             "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";

       Set<ScheduledTask> tasks = new LinkedHashSet<>(4);

       // Determine initial delay
       long initialDelay = scheduled.initialDelay();
       String initialDelayString = scheduled.initialDelayString();
       if (StringUtils.hasText(initialDelayString)) {
          Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
          if (this.embeddedValueResolver != null) {
             initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
          }
          if (StringUtils.hasLength(initialDelayString)) {
             try {
                initialDelay = parseDelayAsLong(initialDelayString);
             }
             catch (RuntimeException ex) {
                throw new IllegalArgumentException(
                      "Invalid initialDelayString value "" + initialDelayString + "" - cannot parse into long");
             }
          }
       }

       // Check cron expression
       String cron = scheduled.cron();
       if (StringUtils.hasText(cron)) {
          String zone = scheduled.zone();
          if (this.embeddedValueResolver != null) {
             cron = this.embeddedValueResolver.resolveStringValue(cron);
             zone = this.embeddedValueResolver.resolveStringValue(zone);
          }
          if (StringUtils.hasLength(cron)) {
             Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
             processedSchedule = true;
             if (!Scheduled.CRON_DISABLED.equals(cron)) {
                TimeZone timeZone;
                if (StringUtils.hasText(zone)) {
                   timeZone = StringUtils.parseTimeZoneString(zone);
                }
                else {
                   timeZone = TimeZone.getDefault();
                }
                tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
             }
          }
       }

       // At this point we don't need to differentiate between initial delay set or not anymore
       if (initialDelay < 0) {
          initialDelay = 0;
       }

       // Check fixed delay
       long fixedDelay = scheduled.fixedDelay();
       if (fixedDelay >= 0) {
          Assert.isTrue(!processedSchedule, errorMessage);
          processedSchedule = true;
          tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
       }
       String fixedDelayString = scheduled.fixedDelayString();
       if (StringUtils.hasText(fixedDelayString)) {
          if (this.embeddedValueResolver != null) {
             fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);
          }
          if (StringUtils.hasLength(fixedDelayString)) {
             Assert.isTrue(!processedSchedule, errorMessage);
             processedSchedule = true;
             try {
                fixedDelay = parseDelayAsLong(fixedDelayString);
             }
             catch (RuntimeException ex) {
                throw new IllegalArgumentException(
                      "Invalid fixedDelayString value "" + fixedDelayString + "" - cannot parse into long");
             }
             tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
          }
       }

       // Check fixed rate
       long fixedRate = scheduled.fixedRate();
       if (fixedRate >= 0) {
          Assert.isTrue(!processedSchedule, errorMessage);
          processedSchedule = true;
          tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
       }
       String fixedRateString = scheduled.fixedRateString();
       if (StringUtils.hasText(fixedRateString)) {
          if (this.embeddedValueResolver != null) {
             fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);
          }
          if (StringUtils.hasLength(fixedRateString)) {
             Assert.isTrue(!processedSchedule, errorMessage);
             processedSchedule = true;
             try {
                fixedRate = parseDelayAsLong(fixedRateString);
             }
             catch (RuntimeException ex) {
                throw new IllegalArgumentException(
                      "Invalid fixedRateString value "" + fixedRateString + "" - cannot parse into long");
             }
             tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
          }
       }

       // Check whether we had any attribute set
       Assert.isTrue(processedSchedule, errorMessage);

       // Finally register the scheduled tasks
       synchronized (this.scheduledTasks) {
          Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
          regTasks.addAll(tasks);
       }
    }
    catch (IllegalArgumentException ex) {
       throw new IllegalStateException(
             "Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
    }
}

在了解处理过程之前,先来看看Scheduled有什么参数

public @interface Scheduled {

    /**
     * A special cron expression value that indicates a disabled trigger: {@value}.
     * <p>This is primarily meant for use with ${...} placeholders, allowing for
     * external disabling of corresponding scheduled methods.
     * @since 5.1
     */
    String CRON_DISABLED = "-";


    /**
     * A cron-like expression, extending the usual UN*X definition to include triggers
     * on the second as well as minute, hour, day of month, month and day of week.
     * <p>E.g. {@code "0 * * * * MON-FRI"} means once per minute on weekdays
     * (at the top of the minute - the 0th second).
     * <p>The special value {@link #CRON_DISABLED "-"} indicates a disabled cron trigger,
     * primarily meant for externally specified values resolved by a ${...} placeholder.
     * @return an expression that can be parsed to a cron schedule
     * @see org.springframework.scheduling.support.CronSequenceGenerator
     */
    String cron() default "";

    /**
     * A time zone for which the cron expression will be resolved. By default, this
     * attribute is the empty String (i.e. the server's local time zone will be used).
     * @return a zone id accepted by {@link java.util.TimeZone#getTimeZone(String)},
     * or an empty String to indicate the server's default time zone
     * @since 4.0
     * @see org.springframework.scheduling.support.CronTrigger#CronTrigger(String, java.util.TimeZone)
     * @see java.util.TimeZone
     */
    String zone() default "";

    /**
     * Execute the annotated method with a fixed period in milliseconds between the
     * end of the last invocation and the start of the next.
     * @return the delay in milliseconds
     */
    long fixedDelay() default -1;

    /**
     * Execute the annotated method with a fixed period in milliseconds between the
     * end of the last invocation and the start of the next.
     * @return the delay in milliseconds as a String value, e.g. a placeholder
     * or a {@link java.time.Duration#parse java.time.Duration} compliant value
     * @since 3.2.2
     */
    String fixedDelayString() default "";

    /**
     * Execute the annotated method with a fixed period in milliseconds between
     * invocations.
     * @return the period in milliseconds
     */
    long fixedRate() default -1;

    /**
     * Execute the annotated method with a fixed period in milliseconds between
     * invocations.
     * @return the period in milliseconds as a String value, e.g. a placeholder
     * or a {@link java.time.Duration#parse java.time.Duration} compliant value
     * @since 3.2.2
     */
    String fixedRateString() default "";

    /**
     * Number of milliseconds to delay before the first execution of a
     * {@link #fixedRate()} or {@link #fixedDelay()} task.
     * @return the initial delay in milliseconds
     * @since 3.2
     */
    long initialDelay() default -1;

    /**
     * Number of milliseconds to delay before the first execution of a
     * {@link #fixedRate()} or {@link #fixedDelay()} task.
     * @return the initial delay in milliseconds as a String value, e.g. a placeholder
     * or a {@link java.time.Duration#parse java.time.Duration} compliant value
     * @since 3.2.2
     */
    String initialDelayString() default "";

}

@Scheduled提供了

  • cron
  • zone
  • fixedDelay、fixedDelayString
  • fixedRate、fixedRateString
  • initialDelay、initialDelayString

processScheduled的执行过程如下:

  1. 根据@Scheduled注解的参数构建任务
  2. 根据对应的任务类型注册成ScheduledTask
  3. 将任务加入到scheduledTasks任务列表

细心且多智的我们肯定还记得ScheduledAnnotationBeanPostProcessor还实现了ApplicationListenerSmartInitializingSingleton

@Override
public void afterSingletonsInstantiated() {
    // Remove resolved singleton classes from cache
    this.nonAnnotatedClasses.clear();

    if (this.applicationContext == null) {
       // Not running in an ApplicationContext -> register tasks early...
       finishRegistration();
    }
}

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
    if (event.getApplicationContext() == this.applicationContext) {
       // Running in an ApplicationContext -> register tasks this late...
       // giving other ContextRefreshedEvent listeners a chance to perform
       // their work at the same time (e.g. Spring Batch's job registration).
       finishRegistration();
    }
}

二者都调用了私有方法#finishRegistration,这个方法是完成注册,将任务队列注册到Spring容器中交由Spring管理

到这里 定时任务知道怎么来的了,但是这和他不运行没啥关系啊?

在Spring依赖包中,我们可以看到这个interface TaskScheduler

【踩坑日记】我的@Scheduled 怎么不运行了?

/**
 * Task scheduler interface that abstracts the scheduling of
 * {@link Runnable Runnables} based on different kinds of triggers.
 *
 * <p>This interface is separate from {@link SchedulingTaskExecutor} since it
 * usually represents for a different kind of backend, i.e. a thread pool with
 * different characteristics and capabilities. Implementations may implement
 * both interfaces if they can handle both kinds of execution characteristics.
 *
 * <p>The 'default' implementation is
 * {@link org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler},
 * wrapping a native {@link java.util.concurrent.ScheduledExecutorService}
 * and adding extended trigger capabilities.
 *
 * <p>This interface is roughly equivalent to a JSR-236
 * {@code ManagedScheduledExecutorService} as supported in Java EE 7
 * environments but aligned with Spring's {@code TaskExecutor} model.
 *
 * @author Juergen Hoeller
 * @since 3.0
 * @see org.springframework.core.task.TaskExecutor
 * @see java.util.concurrent.ScheduledExecutorService
 * @see org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
 */
public interface TaskScheduler {
        /**
         * Schedule the given {@link Runnable}, invoking it whenever the trigger
         * indicates a next execution time.
         * <p>Execution will end once the scheduler shuts down or the returned
         * {@link ScheduledFuture} gets cancelled.
         * @param task the Runnable to execute whenever the trigger fires
         * @param trigger an implementation of the {@link Trigger} interface,
         * e.g. a {@link org.springframework.scheduling.support.CronTrigger} object
         * wrapping a cron expression
         * @return a {@link ScheduledFuture} representing pending completion of the task,
         * or {@code null} if the given Trigger object never fires (i.e. returns
         * {@code null} from {@link Trigger#nextExecutionTime})
         * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted
         * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress)
         * @see org.springframework.scheduling.support.CronTrigger
         */
        @Nullable
        ScheduledFuture<?> schedule(Runnable task, Trigger trigger);
        ……

根据注解所描述的内容说明我们可以知道,这就是定时任务执行的接口,来看看schedule方法的实现

【踩坑日记】我的@Scheduled 怎么不运行了? 先来看看ConcurrentTaskScheduler的实现

@Override
@Nullable
public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
    try {
       if (this.enterpriseConcurrentScheduler) {
          return new EnterpriseConcurrentTriggerScheduler().schedule(decorateTask(task, true), trigger);
       }
       else {
          ErrorHandler errorHandler =
                (this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true));
          return new ReschedulingRunnable(task, trigger, this.scheduledExecutor, errorHandler).schedule();
       }
    }
    catch (RejectedExecutionException ex) {
       throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex);
    }
}

查看ConcurrentTaskScheduler的源码,我们可以看到他的默认构造方法和初始化方法如下

public ConcurrentTaskScheduler() {
    super();
    this.scheduledExecutor = initScheduledExecutor(null);
}

private ScheduledExecutorService initScheduledExecutor(@Nullable ScheduledExecutorService scheduledExecutor) {
    if (scheduledExecutor != null) {
       this.scheduledExecutor = scheduledExecutor;
       this.enterpriseConcurrentScheduler = (managedScheduledExecutorServiceClass != null &&
             managedScheduledExecutorServiceClass.isInstance(scheduledExecutor));
    }
    else {
       this.scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
       this.enterpriseConcurrentScheduler = false;
    }
    return this.scheduledExecutor;
}

可以看到他默认是个单线程的任务提交池,再来看看ThreadPoolTaskScheduler实现的schedule方法

// TaskScheduler implementation

@Override
@Nullable
public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
    ScheduledExecutorService executor = getScheduledExecutor();
    try {
       ErrorHandler errorHandler = this.errorHandler;
       if (errorHandler == null) {
          errorHandler = TaskUtils.getDefaultErrorHandler(true);
       }
       return new ReschedulingRunnable(task, trigger, executor, errorHandler).schedule();
    }
    catch (RejectedExecutionException ex) {
       throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex);
    }
}

从源码可以知道,获取ScheduledExecutorService线程池而后提交执行,来看看他的初始化方法

public void initialize() {
    if (logger.isInfoEnabled()) {
       logger.info("Initializing ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : ""));
    }
    if (!this.threadNamePrefixSet && this.beanName != null) {
       setThreadNamePrefix(this.beanName + "-");
    }
    this.executor = initializeExecutor(this.threadFactory, this.rejectedExecutionHandler);
}
@Override
protected ExecutorService initializeExecutor(
       ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {

    this.scheduledExecutor = createExecutor(this.poolSize, threadFactory, rejectedExecutionHandler);

    if (this.removeOnCancelPolicy) {
       if (this.scheduledExecutor instanceof ScheduledThreadPoolExecutor) {
          ((ScheduledThreadPoolExecutor) this.scheduledExecutor).setRemoveOnCancelPolicy(true);
       }
       else {
          logger.debug("Could not apply remove-on-cancel policy - not a ScheduledThreadPoolExecutor");
       }
    }

    return this.scheduledExecutor;
}

这里的poolSize在spring的自动配置bean的时候进行设置,代码里的默认值是1

@ConditionalOnClass(ThreadPoolTaskScheduler.class)
@Configuration
@EnableConfigurationProperties(TaskSchedulingProperties.class)
@AutoConfigureAfter(TaskExecutionAutoConfiguration.class)
public class TaskSchedulingAutoConfiguration {

    @Bean
    @ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
    @ConditionalOnMissingBean({ SchedulingConfigurer.class, TaskScheduler.class, ScheduledExecutorService.class })
    public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) {
       return builder.build();
    }

    @Bean
    @ConditionalOnMissingBean
    public TaskSchedulerBuilder taskSchedulerBuilder(TaskSchedulingProperties properties,
          ObjectProvider<TaskSchedulerCustomizer> taskSchedulerCustomizers) {
       TaskSchedulerBuilder builder = new TaskSchedulerBuilder();
       builder = builder.poolSize(properties.getPool().getSize());
       builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
       builder = builder.customizers(taskSchedulerCustomizers);
       return builder;
    }

}
@ConfigurationProperties("spring.task.scheduling")
public class TaskSchedulingProperties {

    private final Pool pool = new Pool();
    
    

public static class Pool {

    /**
     * Maximum allowed number of threads.
     */
    private int size = 1;

    public int getSize() {
       return this.size;
    }

    public void setSize(int size) {
       this.size = size;
    }

}

这时候仿佛我们看到了希望,是否执行线程不足以执行定时任务?

【踩坑日记】我的@Scheduled 怎么不运行了?

我们先来确定默认的是哪个任务调度器,在org.springframework.boot.autoconfigure.task包下有个TaskSchedulingAutoConfiguration对象

@ConditionalOnClass(ThreadPoolTaskScheduler.class)
@Configuration
@EnableConfigurationProperties(TaskSchedulingProperties.class)
@AutoConfigureAfter(TaskExecutionAutoConfiguration.class)
public class TaskSchedulingAutoConfiguration {

    @Bean
    @ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
    @ConditionalOnMissingBean({ SchedulingConfigurer.class, TaskScheduler.class, ScheduledExecutorService.class })
    public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) {
       return builder.build();
    }

    @Bean
    @ConditionalOnMissingBean
    public TaskSchedulerBuilder taskSchedulerBuilder(TaskSchedulingProperties properties,
          ObjectProvider<TaskSchedulerCustomizer> taskSchedulerCustomizers) {
       TaskSchedulerBuilder builder = new TaskSchedulerBuilder();
       builder = builder.poolSize(properties.getPool().getSize());
       builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
       builder = builder.customizers(taskSchedulerCustomizers);
       return builder;
    }

}

按照这个发现,我们给spring.task.scheduling.pool.size配上一个适合的值,应该就成了吧

可是呢

可是呢

可是呢

【踩坑日记】我的@Scheduled 怎么不运行了?

宿主机的线程怎么炸了啊!!!

查看执行任务发现有存在长期hold线程的任务,任务执行线程没上限?

org.springframework.boot.autoconfigure.task包下还有个TaskExecutionAutoConfiguration对象

Execution这个词我们就知道了他是配置任务执行线程池的配置

【踩坑日记】我的@Scheduled 怎么不运行了?

来看看他的配置参数是什么

@ConfigurationProperties("spring.task.execution")
public class TaskExecutionProperties {

    private final Pool pool = new Pool();

    ……

    public static class Pool {

        /**
         * Queue capacity. An unbounded capacity does not increase the pool and therefore
         * ignores the "max-size" property.
         */
        private int queueCapacity = Integer.MAX_VALUE;

        /**
         * Core number of threads.
         */
        private int coreSize = 8;

        /**
         * Maximum allowed number of threads. If tasks are filling up the queue, the pool
         * can expand up to that size to accommodate the load. Ignored if the queue is
         * unbounded.
         */
        private int maxSize = Integer.MAX_VALUE;

        /**
         * Whether core threads are allowed to time out. This enables dynamic growing and
         * shrinking of the pool.
         */
        private boolean allowCoreThreadTimeout = true;

        /**
         * Time limit for which threads may remain idle before being terminated.
         */
        private Duration keepAlive = Duration.ofSeconds(60);
        ……

他是个核心线程只有8最大线程数是int最大值的线程池

察看java应用的stack可以发现,确实很多都是task-开头的线程任务,马上修改 spring.task.execution.pool.maxSizespring.task.execution.pool.queueCapacity的值

【踩坑日记】我的@Scheduled 怎么不运行了?

把hold住线程的任务中的代码缺陷修复

果然这就解决了,恢复正常了