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接口的方式实现异步。

相关推荐
一元咖啡26 分钟前
SpringCloud Gateway转发请求到同一个服务的不同端口
spring·spring cloud·gateway
儿时可乖了33 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
ruleslol34 分钟前
java基础概念37:正则表达式2-爬虫
java
Iced_Sheep43 分钟前
干掉 if else 之策略模式
后端·设计模式
xmh-sxh-13141 小时前
jdk各个版本介绍
java
XINGTECODE1 小时前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
天天扭码1 小时前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶1 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露