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

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

相关文章

  • SpringBoot集成QQ第三方登陆的实现

    SpringBoot集成QQ第三方登陆的实现

    这篇文章主要介绍了SpringBoot集成QQ第三方登陆的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-11-11
  • java面向对象的三大特性之一继承用法实例分析

    java面向对象的三大特性之一继承用法实例分析

    这篇文章主要介绍了java面向对象的三大特性之一继承用法,结合实例形式分析了java面向对象程序设计中继承的基本原理与具体使用方法,需要的朋友可以参考下
    2019-11-11
  • Java中的substring()方法使用举例详解

    Java中的substring()方法使用举例详解

    这篇文章主要介绍了Java中的substring()方法使用的相关资料,文中包括其概述、参数、返回值、使用示例、注意事项、常见用法和总结,通过代码介绍的非常详细,需要的朋友可以参考下
    2024-12-12
  • 深入了解Java核心类库--String类

    深入了解Java核心类库--String类

    这篇文章主要为大家详细介绍了java String类定义与使用的相关资料,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能给你带来帮助
    2021-07-07
  • java事件处理模型知识点总结

    java事件处理模型知识点总结

    在本篇文章里小辫给大家分享的是一篇关于java事件处理模型知识点总结内容,有兴趣的朋友们可以学习下。
    2021-01-01
  • spring boot 集成 swagger3及配置方法

    spring boot 集成 swagger3及配置方法

    Swagger 3是一种开源的API描述工具,它可以帮助开发人员设计、构建、文档化和测试API,这篇文章主要介绍了spring boot 集成 swagger3,需要的朋友可以参考下
    2023-05-05
  • 出现java.lang.UnsupportedClassVersionError错误的原因以及解决方法

    出现java.lang.UnsupportedClassVersionError错误的原因以及解决方法

    这篇文章主要给大家介绍了关于出现java.lang.UnsupportedClassVersionError错误的原因以及解决方法,文中通过图文以及代码示例将这个错误介绍的非常详细,需要的朋友可以参考下
    2024-05-05
  • JAVA中通过Redis实现延时任务demo实例

    JAVA中通过Redis实现延时任务demo实例

    Redis在2.0版本时引入了发布订阅(pub/sub)功能,在发布订阅中有一个channel(频道),与消息队列中的topic(主题)类似,可以通过redis的发布订阅者模式实现延时任务功能,实例中会议室预约系统,用户预约管理员审核后生效,如未审批,需要自动变超期未处理,使用延时任务
    2024-08-08
  • java中实体类和JSON对象之间相互转化

    java中实体类和JSON对象之间相互转化

    Java中关于Json格式转化Object,Map,Collection类型和String类型之间的转化在我们实际项目中应用的很是普遍和广泛。最近工作的过程中也是经常有,因此,自己封装了一个类分享给大家。
    2015-05-05
  • 在SpringBoot中通过jasypt进行加密解密的方法

    在SpringBoot中通过jasypt进行加密解密的方法

    今天小编就为大家分享一篇关于在SpringBoot中通过jasypt进行加密解密的方法,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-01-01

最新评论