spring定时器@Scheduled异步调用

文章目录


前言

在springboot中的@schedule默认的线程池中只有一个线程,所以如果在多个方法上加上@schedule的话,此时就会有多个任务加入到延时队列中,因为只有一个线程,所以任务只能被一个一个的执行。
如果有多个定时器,而此时有定时器运行时间过长,就会导致其他的定时器无法正常执行。

代码示例

java 复制代码
@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定时任务执行了

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


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

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

配置文件

yml 复制代码
thread-pool:
  config:
    core-size: 8
    max-size: 16
    queue-capacity: 64
    keep-alive-seconds: 180
java 复制代码
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;
}

线程池

java 复制代码
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;
    }
}

定时器

java 复制代码
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定时任务执行结束

查看输出台可以看出定时器已经异步执行了。但是这里会发现一个问题,可以发现当前定时器任务还没有执行完一轮,下一轮就已经开始了。如果业务中需要用到上一次定时器的结果等情况,则会出现问题。

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

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

改造定时器类

java 复制代码
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实现定时器异步调用

配置文件

java 复制代码
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));
    }
}

定时器类

java 复制代码
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接口的方式实现异步。

相关推荐
一只叫煤球的猫3 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
bobz9654 小时前
tcp/ip 中的多路复用
后端
bobz9654 小时前
tls ingress 简单记录
后端
皮皮林5515 小时前
IDEA 源码阅读利器,你居然还不会?
java·intellij idea
你的人类朋友5 小时前
什么是OpenSSL
后端·安全·程序员
bobz9655 小时前
mcp 直接操作浏览器
后端
前端小张同学7 小时前
服务器部署 gitlab 占用空间太大怎么办,优化思路。
后端
databook8 小时前
Manim实现闪光轨迹特效
后端·python·动效
武子康8 小时前
大数据-98 Spark 从 DStream 到 Structured Streaming:Spark 实时计算的演进
大数据·后端·spark
该用户已不存在9 小时前
6个值得收藏的.NET ORM 框架
前端·后端·.net