spring定时器@Scheduled异步调用方式

 更新时间:2024年11月21日 08:39:00   作者:齐 飞  
在Spring Boot中,@Schedule默认使用单线程执行定时任务,多个定时器会按顺序执行,为实现异步执行,可以通过自定义线程池或实现SchedulingConfigurer接口,使用自定义线程池可以保证多个定时器并发执行

前言

在springboot中的@schedule默认的线程池中只有一个线程,所以如果在多个方法上加上@schedule的话,此时就会有多个任务加入到延时队列中,因为只有一个线程,所以任务只能被一个一个的执行。

如果有多个定时器,而此时有定时器运行时间过长,就会导致其他的定时器无法正常执行。

代码示例

@Component
public class TestTimer {

    @Scheduled(cron = "0/1 * *  * * ? ")
    public void test01()
    {
        Date date = new Date();
        log.info((new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) + "test01定时任务执行开始"));
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info((new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) + "test01定时任务执行结束"));
    }

    @Scheduled(cron = "0/1 * *  * * ? ")
    public void test02()
    {
        Date date = new Date();
        log.info((new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) + "test02定时任务执行了"));
    }
}

注意需要在启动类上加上@EnableScheduling

输出台

2024-08-19 19:07:52.010  INFO 10372 --- [   scheduling-1] com.qbh.timer.TestTimer                  : 2024-08-19 19:07:52test02定时任务执行了
2024-08-19 19:07:52.010  INFO 10372 --- [   scheduling-1] com.qbh.timer.TestTimer                  : 2024-08-19 19:07:52test01定时任务执行开始
2024-08-19 19:07:55.024  INFO 10372 --- [   scheduling-1] com.qbh.timer.TestTimer                  : 2024-08-19 19:07:52test01定时任务执行结束
2024-08-19 19:07:55.024  INFO 10372 --- [   scheduling-1] com.qbh.timer.TestTimer                  : 2024-08-19 19:07:55test02定时任务执行了
2024-08-19 19:07:56.002  INFO 10372 --- [   scheduling-1] com.qbh.timer.TestTimer                  : 2024-08-19 19:07:56test01定时任务执行开始
2024-08-19 19:07:59.016  INFO 10372 --- [   scheduling-1] com.qbh.timer.TestTimer                  : 2024-08-19 19:07:56test01定时任务执行结束
2024-08-19 19:07:59.016  INFO 10372 --- [   scheduling-1] com.qbh.timer.TestTimer                  : 2024-08-19 19:07:59test02定时任务执行了
2024-08-19 19:08:00.014  INFO 10372 --- [   scheduling-1] com.qbh.timer.TestTimer                  : 2024-08-19 19:08:00test01定时任务执行开始
2024-08-19 19:08:03.022  INFO 10372 --- [   scheduling-1] com.qbh.timer.TestTimer                  : 2024-08-19 19:08:00test01定时任务执行结束
2024-08-19 19:08:03.022  INFO 10372 --- [   scheduling-1] com.qbh.timer.TestTimer                  : 2024-08-19 19:08:03test02定时任务执行了

从打印的日志也可以看出来,两个定时器共用一个线程。

此时就需要让定时器使用异步的方式进行,以下为实现方式:

使用自定义线程池实现异步代码

配置文件

thread-pool:
  config:
    core-size: 8
    max-size: 16
    queue-capacity: 64
    keep-alive-seconds: 180
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author qf
 * @since 2024/08/20
 */
@Component
@ConfigurationProperties(prefix = "thread-pool.config")
@Data
public class TestThreadPoolConfig {
    private Integer coreSize;
    private Integer maxSize;
    private Integer queueCapacity;
    private Integer keepAliveSeconds;
}

线程池

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.RejectedExecutionHandler;

/**
 * @author qf
 */
@Configuration
@EnableAsync
public class ThreadPoolConfig {

    @Autowired
    private TestThreadPoolConfig testThreadPoolConfig;

    @Bean(name = "testExecutor")
    public ThreadPoolTaskExecutor testThreadPoolExecutor() {
        return getAsyncTaskExecutor("test-Executor-", testThreadPoolConfig.getCoreSize(),
                testThreadPoolConfig.getMaxSize(), testThreadPoolConfig.getQueueCapacity(),
                testThreadPoolConfig.getKeepAliveSeconds(), null);
    }

    /**
     * 统一异步线程池
     *
     * @param threadNamePrefix
     * @param corePoolSize
     * @param maxPoolSize
     * @param queueCapacity
     * @param keepAliveSeconds
     * @param rejectedExecutionHandler 拒接策略 没有填null
     * @return
     */
    private ThreadPoolTaskExecutor getAsyncTaskExecutor(String threadNamePrefix, int corePoolSize, int maxPoolSize, int queueCapacity, int keepAliveSeconds, RejectedExecutionHandler rejectedExecutionHandler) {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(corePoolSize);
        taskExecutor.setMaxPoolSize(maxPoolSize);
        taskExecutor.setQueueCapacity(queueCapacity);
        taskExecutor.setThreadPriority(Thread.MAX_PRIORITY);//线程优先级
        taskExecutor.setDaemon(false);//是否为守护线程
        taskExecutor.setKeepAliveSeconds(keepAliveSeconds);
        taskExecutor.setThreadNamePrefix(threadNamePrefix);
        taskExecutor.setRejectedExecutionHandler(rejectedExecutionHandler);
        taskExecutor.initialize();//线程池初始化
        return taskExecutor;
    }
}

定时器

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author qf
 */
@Slf4j
@Component
public class TestTimer {

    @Async("testExecutor")
    @Scheduled(cron = "0/1 * *  * * ? ")
    public void test01() {
        Date date = new Date();
        log.info((new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) + "test01定时任务执行开始"));
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info((new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) + "test01定时任务执行结束"));
    }

    @Async("testExecutor")
    @Scheduled(cron = "0/1 * *  * * ? ")
    public void test02() {
        Date date = new Date();
        log.info((new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) + "test02定时任务执行了"));
    }
}

输出台结果

2024-08-20 19:33:20.020  INFO 4420 --- [test-Executor-1] com.qbh.timer.TestTimer                  : 2024-08-20 19:33:20test01定时任务执行开始
2024-08-20 19:33:20.020  INFO 4420 --- [test-Executor-2] com.qbh.timer.TestTimer                  : 2024-08-20 19:33:20test02定时任务执行了
2024-08-20 19:33:21.002  INFO 4420 --- [test-Executor-4] com.qbh.timer.TestTimer                  : 2024-08-20 19:33:21test02定时任务执行了
2024-08-20 19:33:21.002  INFO 4420 --- [test-Executor-3] com.qbh.timer.TestTimer                  : 2024-08-20 19:33:21test01定时任务执行开始
2024-08-20 19:33:22.015  INFO 4420 --- [test-Executor-5] com.qbh.timer.TestTimer                  : 2024-08-20 19:33:22test01定时任务执行开始
2024-08-20 19:33:22.015  INFO 4420 --- [test-Executor-6] com.qbh.timer.TestTimer                  : 2024-08-20 19:33:22test02定时任务执行了
2024-08-20 19:33:23.009  INFO 4420 --- [test-Executor-7] com.qbh.timer.TestTimer                  : 2024-08-20 19:33:23test01定时任务执行开始
2024-08-20 19:33:23.009  INFO 4420 --- [test-Executor-8] com.qbh.timer.TestTimer                  : 2024-08-20 19:33:23test02定时任务执行了
2024-08-20 19:33:23.025  INFO 4420 --- [test-Executor-1] com.qbh.timer.TestTimer                  : 2024-08-20 19:33:20test01定时任务执行结束
2024-08-20 19:33:24.003  INFO 4420 --- [test-Executor-3] com.qbh.timer.TestTimer                  : 2024-08-20 19:33:21test01定时任务执行结束

查看输出台可以看出定时器已经异步执行了。

但是这里会发现一个问题,可以发现当前定时器任务还没有执行完一轮,下一轮就已经开始了

如果业务中需要用到上一次定时器的结果等情况,则会出现问题。

解决上一轮定时器任务未执行完成,下一轮就开始执行的问题

本人暂时想到的方式是通过加锁的方式,当上一轮未执行完时,下一轮阻塞等待上一轮执行。

改造定时器类

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author qf
 */
@Slf4j
@Component
public class TestTimer {

    private final ReentrantLock timerLock = new ReentrantLock();
    @Async("testExecutor")
    @Scheduled(cron = "0/1 * *  * * ? ")
    public void test01() {
        timerLock.lock();
        Date date = new Date();
        log.info((new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) + "test01定时任务执行开始"));
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            timerLock.unlock();//释放锁
        }
        log.info((new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) + "test01定时任务执行结束"));
    }

    @Async("testExecutor")
    @Scheduled(cron = "0/1 * *  * * ? ")
    public void test02() {
        Date date = new Date();
        log.info((new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) + "test02定时任务执行了"));
    }
}

输出台

2024-08-20 19:55:26.007  INFO 7752 --- [test-Executor-1] com.qbh.timer.TestTimer                  : 2024-08-20 19:55:26test02定时任务执行了
2024-08-20 19:55:26.007  INFO 7752 --- [test-Executor-2] com.qbh.timer.TestTimer                  : 2024-08-20 19:55:26test01定时任务执行开始
2024-08-20 19:55:27.004  INFO 7752 --- [test-Executor-3] com.qbh.timer.TestTimer                  : 2024-08-20 19:55:27test02定时任务执行了
2024-08-20 19:55:28.014  INFO 7752 --- [test-Executor-5] com.qbh.timer.TestTimer                  : 2024-08-20 19:55:28test02定时任务执行了
2024-08-20 19:55:29.009  INFO 7752 --- [test-Executor-2] com.qbh.timer.TestTimer                  : 2024-08-20 19:55:26test01定时任务执行结束
2024-08-20 19:55:29.009  INFO 7752 --- [test-Executor-4] com.qbh.timer.TestTimer                  : 2024-08-20 19:55:29test01定时任务执行开始
2024-08-20 19:55:29.009  INFO 7752 --- [test-Executor-7] com.qbh.timer.TestTimer                  : 2024-08-20 19:55:29test02定时任务执行了
2024-08-20 19:55:30.004  INFO 7752 --- [test-Executor-1] com.qbh.timer.TestTimer                  : 2024-08-20 19:55:30test02定时任务执行了
2024-08-20 19:55:31.015  INFO 7752 --- [test-Executor-5] com.qbh.timer.TestTimer                  : 2024-08-20 19:55:31test02定时任务执行了
2024-08-20 19:55:32.009  INFO 7752 --- [test-Executor-4] com.qbh.timer.TestTimer                  : 2024-08-20 19:55:29test01定时任务执行结束
2024-08-20 19:55:32.009  INFO 7752 --- [test-Executor-7] com.qbh.timer.TestTimer                  : 2024-08-20 19:55:32test02定时任务执行了
2024-08-20 19:55:32.009  INFO 7752 --- [test-Executor-6] com.qbh.timer.TestTimer                  : 2024-08-20 19:55:32test01定时任务执行开始

通过输出台可以看出下一轮的定时器会等待上一轮结束释放锁后才会执行。

使用SchedulingConfigurer实现定时器异步调用

配置文件

import org.springframework.boot.autoconfigure.batch.BatchProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.lang.reflect.Method;
import java.util.concurrent.Executors;

@Configuration
public class ScheduleConfig implements SchedulingConfigurer {

    /**
     * 计算出带有Scheduled注解的方法数量,如果该数量小于默认池大小(20),则使用默认线程池核心数大小20。
     * @param taskRegistrar the registrar to be configured.
     */
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        Method[] methods = BatchProperties.Job.class.getMethods();
        int defaultPoolSize = 20;
        int corePoolSize = 0;
        if (methods != null && methods.length > 0) {
            System.out.println(methods.length);
            for (Method method : methods) {
                Scheduled annotation = method.getAnnotation(Scheduled.class);
                if (annotation != null) {
                    corePoolSize++;
                }
            }
            if (defaultPoolSize > corePoolSize) {
                corePoolSize = defaultPoolSize;
            }
        }
        taskRegistrar.setScheduler(Executors.newScheduledThreadPool(corePoolSize));
    }
}

定时器类

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author qf
 */
@Slf4j
@Component
public class TestTimer {

    @Scheduled(cron = "0/1 * *  * * ? ")
    public void test01() {
        Date date = new Date();
        log.info((new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) + "test01定时任务执行开始"));
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info((new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) + "test01定时任务执行结束"));
    }

    @Scheduled(cron = "0/1 * *  * * ? ")
    public void test02() {
        Date date = new Date();
        log.info((new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) + "test02定时任务执行了"));
    }
}

输出台结果

2024-08-20 20:18:58.002  INFO 24744 --- [pool-2-thread-2] com.qbh.timer.TestTimer                  : 2024-08-20 20:18:58test01定时任务执行开始
2024-08-20 20:18:58.002  INFO 24744 --- [pool-2-thread-1] com.qbh.timer.TestTimer                  : 2024-08-20 20:18:58test02定时任务执行了
2024-08-20 20:18:59.014  INFO 24744 --- [pool-2-thread-1] com.qbh.timer.TestTimer                  : 2024-08-20 20:18:59test02定时任务执行了
2024-08-20 20:19:00.006  INFO 24744 --- [pool-2-thread-3] com.qbh.timer.TestTimer                  : 2024-08-20 20:19:00test02定时任务执行了
2024-08-20 20:19:01.005  INFO 24744 --- [pool-2-thread-1] com.qbh.timer.TestTimer                  : 2024-08-20 20:19:01test02定时任务执行了
2024-08-20 20:19:01.005  INFO 24744 --- [pool-2-thread-2] com.qbh.timer.TestTimer                  : 2024-08-20 20:18:58test01定时任务执行结束
2024-08-20 20:19:02.013  INFO 24744 --- [pool-2-thread-3] com.qbh.timer.TestTimer                  : 2024-08-20 20:19:02test01定时任务执行开始

以上可以看出该方法也可以实现定时器异步执行,并且当上一轮定时器没有执行完时,下一轮会等待上一轮完成后执行。

总结

在Springboot中的@schedule默认的线程池中只有一个线程,当有多个定时器时,只会先执行其中的一个,其他定时器会加入到延时队列中,等待被执行。

Springboot实现定时器异步的方式有

  1. 通过自定义线程池的方式实现异步。
  2. 通过实现SchedulingConfigurer接口的方式实现异步。

这些仅为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • jdk中String类设计成final的原由

    jdk中String类设计成final的原由

    为什么jdk中把 String 类设计成final,主要是为了“ 效率 ”和“安全性”的缘故,若 String 允许被继承, 由于它的高度被使用率, 可能会降低程序的性能,所以String被定义成final,需要了解的朋友可以参考下
    2013-01-01
  • java中使用url进行编码和解码

    java中使用url进行编码和解码

    这篇文章主要介绍了java中使用url进行编码和解码,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-08-08
  • request如何获取body的json数据

    request如何获取body的json数据

    这篇文章主要介绍了request如何获取body的json数据操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06
  • spring单例如何改多例

    spring单例如何改多例

    这篇文章主要介绍了spring单例如何改多例,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • 如何用120行Java代码写一个自己的区块链

    如何用120行Java代码写一个自己的区块链

    这篇文章就是帮助你使用 Java 语言来实现一个简单的区块链,用不到 120 行代码来揭示区块链的原理,感兴趣的就一起来了解一下
    2019-06-06
  • 使用filebeat收集并解析springboot日志过程示例

    使用filebeat收集并解析springboot日志过程示例

    这篇文章主要为大家介绍了使用filebeat收集并解析springboot日志实现过程示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • Java多线程之简单模拟售票功能

    Java多线程之简单模拟售票功能

    这篇文章主要介绍了Java多线程之简单模拟售票功能,文中有非常详细的代码示例,对正在学习java的小伙伴们有很好地帮助,需要的朋友可以参考下
    2021-04-04
  • 一文吃透Spring Cloud gateway自定义错误处理Handler

    一文吃透Spring Cloud gateway自定义错误处理Handler

    这篇文章主要为大家介绍了一文吃透Spring Cloud gateway自定义错误处理Handler方法,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-03-03
  • Maven中的profiles使用及说明

    Maven中的profiles使用及说明

    这篇文章主要介绍了Maven中的profiles使用及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-06-06
  • Java调用打印机的2种方式举例(无驱/有驱)

    Java调用打印机的2种方式举例(无驱/有驱)

    我们平时使用某些软件或者在超市购物的时候都会发现可以使用打印机进行打印,这篇文章主要给大家介绍了关于Java调用打印机的2种方式,分别是无驱/有驱的相关资料,需要的朋友可以参考下
    2023-11-11

最新评论