spring任务执行器与任务调度器(TaskExecutor And TaskScheduler)

对于多线程及周期性调度相关的操作,spring框架提供了TaskExecutor和TaskScheduler接口为异步执行和任务调度。并提供了相关实现类给开发者使用。(只记录采用注解的使用形式,对于XML的使用形式不做笔记。)
Spring官方对TaskExecutor的相关解释:

Spring的TaskExecutor接口与java.util.concurrent.Executor接口相同。该接口具有单个方法(execute(Runnable task)),该方法根据线程池的语义和配置接受要执行的任务。

二话(写)不说,自说(写)自话(字)直接拐上了

TaskExecutor接口相关实现类

实现类名 对应解释(直接甩翻译了)
SyncTaskExecutor 该实现类不会执行异步调用。 相反,每次调用都在调用的线程中进行(翻译过来也即同步任务执行器)。 它主要用于不需要多线程的情况,例如在简单的测试用例中。
SimpleAsyncTaskExecutor 此实现不会重用任何线程。 相反,它为每次调用启动一个新线程。 但是,它确实支持并发限制,该限制会阻止超出限制的任何调用,直到释放插槽为止。(说简单了,就是要使用了直接创建一个线程)
ConcurrentTaskExecutor 此实现是java.util.concurrent.Executor实例的适配器。很少需要直接使用ConcurrentTaskExecutor(官网自己都觉得很少使用,不过相对于ThreadPoolTaskExecutor,官网推荐如果ThreadPoolTaskExecutor不够灵活,无法满足需求,则可以使用ConcurrentTaskExecutor)
ThreadPoolTaskExecutor 杀手锏级的任务调度器(最常用),可以说已经足够满足我们的需求了(除非,非常非常特例才使用ConcurrentTaskExecutor)。官网翻译重要片段:公开了bean属性,用于配置java.util.concurrent.ThreadPoolExecutor并将其包装在TaskExecutor中
WorkManagerTaskExecutor 此实现使用CommonJ WorkManager作为其后备服务提供程序,并且是在Spring应用程序上下文中在WebLogic或WebSphere上设置基于CommonJ的线程池集成的中心便利类。
DefaultManagedTaskExecutor 此实现在JSR-236兼容的运行时环境(例如Java EE 7+应用程序服务器)中使用JNDI获取的ManagedExecutorService,为此目的替换CommonJ WorkManager。(说明了就是依赖环境)

其中可能今后工作中会用到的(包括测试):SyncTaskExecutor、SimpleAsyncTaskExecutor、ConcurrentTaskExecutor、ThreadPoolTaskExecutor此四个实现类。重点关注ThreadPoolTaskExecutor此类。
:以上均实现了spring提供的TaskExecutor接口。

Spring官方对TaskSheduler接口的相关解释:

用于在将来的某个时间点调度任务。

TaskScheduler接口相关实现类

实现类名 对应解释
ConcurrentTaskScheduler 该类实际继承了ConcurrentTaskExecutor对象。只是实现了TaskScheduler接口,增加了相关定时调度任务的方法。
ThreadPoolTaskScheduler spring对该类设计原则同ThreadPoolTaskExecutor类。是为了定时调度任务不依赖相关的运行容器(例如weblogic、WebSphere等)。其底层委托给ScheduledExecutorService,向外暴露相关的常见bean配置属性。

接下来对以上相应的实现类通过注解的形式进行相应的测试
首先进行解释要用到的几个注解,对这几个注解总结如下表

注解名 解释
@EnableAsync 开启异步执行。官方文档中解释:该注解添加到@Configuration标注的类上以开始异步执行。开启后@Async标注的方法或类即可异步执行。
@EnableScheduling 开启定时调度。官方文档解释也是配合@Configuration一起使用。开启后@Scheduled注解标注的方法即可自动定时(或延迟)执行。
@Async 异步执行注解。可标注类和方法。标注类时,则该类下所有方法均可使用异步执行。标注方法时,则该方法可使用异步执行。当标注有@Configuration注解的配置类上标注了@EnableAsync注解后即可生效。
@Scheduled 标注相关方法后,如果配置类标注了@EnableScheduling后即可开启定时调度任务。
  • 接下来先测试异步执行器(TaskExecutor)

使用方式1(直接使用spring容器中的ThreadPoolTaskExecutor):

先通过@Bean注解将ThreadPoolTaskExecutor放入Spring容器中:

@Configuration
public class ExecutorBean {
    /**
     * 执行器ThreadPoolTaskExecutor
     */
    @Bean
    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10); // 设置核心线程池大小(这里设置为初始化10个)
        executor.setMaxPoolSize(30); // 设置最大线程池大小(当核心线程池不够用时候,会自动在原基础上增加。最大为30个)
        executor.setQueueCapacity(2000); // 设置队列容量为2000个。
        return executor;
    }
}

然后在标注有@Configuration注解的配置类或SpringBoot启动类上添加@EntityAsync注解。这里在标有@SpringBootApplication注解的SpringBoot启动类的入口方法上添加@EntityAsync注解。

@SpringBootApplication
@EnableAsync
public class ScheduleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ScheduleApplication.class, args);
    }
}

然后在需要采取异步执行的方法上(或类上,此测试使用方法)标注@Async注解:

@Async(value="threadPoolTaskExecutor")
public void testThreadPoolTaskExecutor() {
    System.err.println("---  testThreadPoolTaskExecutor  --");
}

测试用例:

@Test
public void testThreadPoolTaskExecutor() {
    System.err.println("--- 开始 ---");
    taskExecutorService.testThreadPoolTaskExecutor();
    System.err.println("--- 结束 ---");
}

执行结果:

--- 开始 ---
--- 结束 ---
---  testThreadPoolTaskExecutor  --

使用方式2(通过修改默认的线程池配置,即实现AsyncConfigurer接口,并重写其中的getAsyncExecutor方法[因为是JDK8提供的default方法,所以才称为重写]):
首先,先实现AsyncConfigurer接口,重写getAsyncExecutor方法并将此实现类作为配置类装载进spring容器中(记:对于void返回类型,异常未被捕获且无法传输,所以getAsyncUncaughtExceptionHandler方法用于处理异步调用后出现异常的情况。这里仅仅记录未出现异常的测试),同时添加@EnableAsync开启可异步调用(也可以在springBoot启动类中的入口方法上添加)。

@Configuration
@EnableAsync
public class CustomAsyncConfigurer implements AsyncConfigurer {
    /**
     * 设置线程池相关的配置
     * @return ThreadPoolTaskExecutor
     */
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10); // 设置核心线程池大小(这里设置为初始化10个)
        executor.setMaxPoolSize(30); // 设置最大线程池大小(当核心线程池不够用时候,会自动在原基础上增加。最大为30个)
        executor.setQueueCapacity(2000); // 设置队列容量为2000个。
        executor.initialize();
        return executor;
    }
}

然后可以直接使用@Async注解标注需要异步执行的方法(或类上,此测试使用方法)

@Async
public void testThreadPoolTaskExecutor(Long id) {
    System.err.println("---  testThreadPoolTaskExecutor  --" + id);
}

测试用例:

@Test
public void testThreadPoolTaskExecutor() {
    System.err.println("--- 开始前 ---");
    System.err.println("--- 开始 ---");
    for(int i =0;i<20;i++) {
        taskExecutorService.testThreadPoolTaskExecutor(new Long(i));
    }
    System.err.println("--- 结束 ---");
    System.err.println("--- 结束后 ---");
}

测试结果:

--- 开始前 ---
--- 开始 ---
---  testThreadPoolTaskExecutor  --0
---  testThreadPoolTaskExecutor  --1
---  testThreadPoolTaskExecutor  --2
---  testThreadPoolTaskExecutor  --3
---  testThreadPoolTaskExecutor  --4
---  testThreadPoolTaskExecutor  --5
---  testThreadPoolTaskExecutor  --6
---  testThreadPoolTaskExecutor  --7
---  testThreadPoolTaskExecutor  --8
---  testThreadPoolTaskExecutor  --9
---  testThreadPoolTaskExecutor  --10
---  testThreadPoolTaskExecutor  --11
---  testThreadPoolTaskExecutor  --12
---  testThreadPoolTaskExecutor  --13
---  testThreadPoolTaskExecutor  --14
---  testThreadPoolTaskExecutor  --17
---  testThreadPoolTaskExecutor  --16
---  testThreadPoolTaskExecutor  --15
--- 结束 ---
--- 结束后 ---
---  testThreadPoolTaskExecutor  --19
---  testThreadPoolTaskExecutor  --18

**使用方式3:因为ThreadPoolTaskScheduler实现了TaskExecutor相关的接口。所以同样可以用ThreadPoolTaskScheduler替换ThreadPoolTaskExecutor来调用异步执行器。
同样实现AsyncConfigurer接口,重写getAsyncExecutor方法并将此实现类作为配置类装载进spring容器中。只是此时用ThreadPoolTaskScheduler替换ThreadPoolTaskExecutor类作为任务执行器

@Configuration
@EnableAsync
public class CustomAsyncConfigurer implements AsyncConfigurer {
    /**
     * 设置线程池相关的配置
     * @return ThreadPoolTaskExecutor
     */
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
        executor.setPoolSize(30);
        executor.initialize();
        return executor;
    }
}

然后用@Asnyc标注方法

@Async
public void testThreadPoolTaskScheduler(Long id) {
    System.err.println("---  testThreadPoolTaskExecutor  --" + id);
}

测试

@Test
public void testThreadPoolTaskScheduler() {
    System.err.println("--- 开始前 ---");
    System.err.println("--- 开始 ---");
    for(int i =0;i<20;i++) {
        taskExecutorService.testThreadPoolTaskScheduler(new Long(i));
    }
    System.err.println("--- 结束 ---");
    System.err.println("--- 结束后 ---");
}

经过测试,用例3没啥效果...测试方式可能有误。同样的异步调用ThreadPoolTaskScheduler类可能不能当做ThreadPoolTaskExecutor 类使用。虽然同样实现了TaskExecutor接口(看来得看底层源码了,现在仅仅记录下来先)

  • 现在来测试定时调度器(TaskScheduler)

使用方式1(直接使用@EnableScheduling开启定时调度任务,然后对需要定时调度的方法用@Sheduled注解标注):
直接在SpringBoot启动器类中的入口方法上标注或标注了@Configuration注解的配置类上使用@EnableScheduling注解

//在启动类上添加
@SpringBootApplication
@EnableScheduling
public class ScheduleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ScheduleApplication.class, args);
    }
}

//或在在配置类上标注
@Configuration
@EnableScheduling
public class CustomScheduleConfigurer implements AsyncConfigurer {
//...
}

需要在定时任务的方法上添加@Scheduled

@Scheduled(fixedRate = 300)  //表示每300毫秒调用一次该方法
@Async
@Override
public void threadPoolTaskScheduler() {
    System.err.println("---  threadPoolTaskScheduler  --");
}

测试用例

@Test
public void schedulers() throws Exception {
    Thread.sleep(50000000);   //让线程睡上一段时间,以便查看效果
}

因为应用一启动后定时调度器便会开始执行。如果测试用例不使用线程睡眠的话程序会一瞬间执行结束,有可能看不到效果。
:这里添加了@async注解,表示此方法定时调用时使用异步方式执行。
:@Scheduled注解支持Quartz的CRON表达式来规划定时任务:SPRING官网示例)。
spring框架支持Quartz来使用定时调度任务
这里简单记录其各个表达式含义:

@Scheduled注解的各个参数与其含义
参数名 类型 含义
cron String 使用表达式的方式定义任务执行时间
zone String 可以通过它设定区域时间(时区)
fixedDelay long 表示从上一个任务完成后到下一个任务开始时的时间间隔,单位毫秒
fixedDelayString String 同fixedDelay,只是为字符串值,可使用SPEL表达式来引入配置文件配置
inittalDelay long 初始化延时时间。spring容器初始化后首次任务延时多久开始执行,单位毫秒
inittalDelayString String 同 inittalDelay,值为字符串值。可使用SPEL表达式来引入配置文件配置
fixedRate long 每次执行任务的间隔时间,单位毫秒
fixedRateString String 每次执行任务的间隔时间,单位为字符串值。可使用SPEL表达式来引入配置文件配置

CRON表达式有6~7个空格分隔得时间元素,按顺序依次为:
秒 分 时 天 月 星期 年 其中年可以是不用配置的元素。
例:
0 0 0 ? * WED
表示每个星期三0点整执行任务。其中因为天和星期会产生定义上的冲突。所以采用了通配符。以下为通配符含义

通配符符号 含义
* 表示任意
? 表示不指定,用于处理天和星期的冲突
- 指定时间区间
/ 指定时间间隔
L 指最后
# 第几
, 列举多个项

以下为几个难点的例子:

示例 描述
0 0/6 19,22 * * ? 每天19点至19点59和22点至22点59分俩时间段内每6分钟执行一次
0 10-16 22 * * ? 每天的22点至22点16分每分钟执行一次
0 0 19 ? * MON-FRI 周一到周五的每天19点执行一次
0 23 23 ? * 5L 2019-2022 2019年至2022年的每月最后一个周四的23点23分执行
0 16 22 ? * 6#2 每月第二周周五的22点16分执行一次

使用方式2(实现SchedulingConfigurer配置类,规定定时任务)
首先实现SchedulingConfigurer配置类,并启用定时调度任务

@Configuration
@EnableScheduling
public class CustomSchedulerConfigurer implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        IntervalTask task = new IntervalTask(new Runnable() {
            @Override
            public void run() {
                System.err.println("----间隔执行----"); 
            }
        }, 2000);
        taskRegistrar.addFixedDelayTask(task);    //taskRegistrar有更多相关方法来执行定时调度任务。测试用例先做简单记录
    }
}

然后进行测试

@Test
public void schedulers() throws Exception {
    Thread.sleep(50000000);    //同样让线程睡上一段时间,以便查看效果
}

测试中并未处理异步执行后如果出现异常的情况。异常情况发生后如何处理之后再做记录。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343