Java多线程之定时任务 以及 SpringBoot多线程实现定时任务、以及分享动态实现定时任务
- 1. 基于单线程的定时器——简单介绍 Timer 中的 schedule 与 scheduleAtFixedRate
- 1.1 前言
- 1.2 先说 schedule
- 附代码:
- 1.3 schedule 与 scheduleAtFixedRate 的区别
- 小结
- 附代码
- 2. 基于多线程的定时器——ScheduledExecutorService
- 2.1 executorService.schedule
- 2.2 scheduleAtFixedRate 与 scheduleWithFixedDelay 区别
- 2.2.1 executorService.scheduleAtFixedRate
- 2.2.2 executorService.scheduleWithFixedDelay
- 2.2.3 小节
- 3. 基于多线程的定时器(SpringBoot)
- 3.1 提前了解 cron表达式和@Async异步
- 3.2 @EnableScheduling 与 @Scheduled
- 3.3 @Scheduled 定时任务例子
- 3.3.1 关于 cron 表达式的
- 3.3.1.1 cron 例1-->单个任务
- (1)简单分析@Schedule默认定时任务的线程
- 3.3.1.2 cron 例2-->多个任务(非异步)
- 3.3.1.3 cron 例3-->多个任务(异步@Async)
- 3.3.1.4 cron 例4-->多个任务(异步@Async——自定义线程池)
- 3.3.1.5 从配置文件读取 cron表达式 以及停掉 cron任务
- (1)cron 例5-->用符号 “ - ”控制停止定时任务
- (2)cron 例6-->手动控制定时任务的开启和停止
- a. 分析 @EnableScheduling
- a. 配置文件设置定时任务的开关
- 3.3.2 其他类型参数——简单说
- 4. springboot动态定时任务的实现
- 5. 附项目工程代码
1. 基于单线程的定时器——简单介绍 Timer 中的 schedule 与 scheduleAtFixedRate
1.1 前言
- Timer,一般是用来做延时的任务或者循环定时执行的任务。
使用 Timer 的时候,必须要有一个 TimerTask 去执行任务,这是一个实现了Runnable接口的线程,run 方法里面就是我们自己定义的线程需要做的任务。
1.2 先说 schedule
- 我们简单说4种情况,其实也就两个重载方法:
- ①
timer.schedule(timerTask,0);
,没用延迟,立即执行task,且只执行一次,线程还在。 代码和效果直接看图 - ②
timer.schedule(timerTask,5000);
,延迟5秒执行task,且只执行一次,线程还在。代码和效果直接看图 - ③
timer.schedule(timerTask,0,60000);
,0表示没用延迟,立即执行,然后60000表示每隔1分钟执行一次task。代码和效果直接看图 - ④
timer.schedule(timerTask,5000,60000);
,延迟5秒后执行task,然后每隔1分钟执行一次task,代码和效果直接看图:
附代码:
package com.liu.susu.thread.task.timer;
import java.time.LocalDateTime;
import java.util.Timer;
import java.util.TimerTask;
/**
* @FileName TimeTaskTest1
* @Description
* @Author susu
* @date 2022-03-07
**/
public class TimerTaskTest1 {
public static void main(String[] args) {
Timer timer = new Timer();
TimerTask timerTask = new TimerTask() {//TimerTask 是实现 Runnable 接口的一个线程
@Override
public void run() {
System.out.println("task-->hello world!-->"+LocalDateTime.now());
}
};
System.out.println("begin-->"+LocalDateTime.now());
// timer.schedule(timerTask,0);//没用延迟,立即执行task,且只执行一次,线程还在
// timer.schedule(timerTask,5000);//延迟5秒执行task,,且只执行一次,线程还在
// timer.schedule(timerTask,0,60000);//0表示没用延迟,立即执行,然后60000表示每隔1分钟执行一次task
timer.schedule(timerTask,5000,60000);//延迟5秒后执行task,然后每隔1分钟执行一次task
}
}
1.3 schedule 与 scheduleAtFixedRate 的区别
- 关于上述介绍的 schedule 的4种调用情况,scheduleAtFixedRate 同样也有,最终效果也是一致的,我们这里就不介绍了,说说它两不一样的地方。什么时候不一样呢?当给他们设置指定开始任务的时间时,它们的区别就有了。
主要是public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period)
方法 与public void schedule(TimerTask task, Date firstTime, long period)
方法的区别。 - 我们先看官网怎么说的:
- 如果不是很明白看我们下面的效果图:
小结
- void schedule(TimerTask task, Date firstTime,long period)方法的任务的计划执行时间是从第一次实际执行任务开始计算的。
如果执行任务的时间没有被延时,那么下一次任务的执行时间参考上一次任务的“开始”时的时间来计算 - scheduleAtFixedRate 会计算首次调用时间,在执行时会先计算出错过的时间内 task 应执行的次数,再去按设定频率去执行。
- 具体原理,还需要大家看源码分析!
附代码
package com.liu.susu.thread.task.timer;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/**
* @FileName TimerTaskTest2
* @Description
* @Author susu
* @date 2022-03-07
**/
public class TimerTaskTest2 {
public static void main(String[] args) {
Timer timer = new Timer();
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
System.out.println("task-->"+ LocalDateTime.now());
}
};
Date firstDateTime = getFirstDateTime();
System.out.println("firstDateTime-->"+firstDateTime+"<=====>now-->"+LocalDateTime.now()+"\n");
timer.schedule(timerTask,firstDateTime,10000);
timer.scheduleAtFixedRate(timerTask,firstDateTime,10000);
}
public static Date getFirstDateTime(){
String dateStr = "2022-03-07 16:30:34";
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.parse(dateStr, dateTimeFormatter);
Date date = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
return date;
}
}
2. 基于多线程的定时器——ScheduledExecutorService
- ScheduledExecutorService 是一个接口,实现类是ScheduledThreadPoolExecutor,是通过 Executors 自动创建线程池的一种方式(newScheduledThreadPool 方式)
- 关于线程池的创建可以看:
详解Java多线程之线程池. - 关于 ScheduledThreadPoolExecutor,我们下面就简单说3个方法,完整代码如下:
package com.liu.susu.thread.task.pool;
import java.time.LocalDateTime;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* @FileName ScheduledThreadPoolTest
* @Description
* @Author susu
* @date 2022-03-07
**/
public class ScheduledThreadPoolTest {
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
System.out.println("当前时间是-->"+LocalDateTime.now());
//1.schedule--->只延迟执行,且只执行一次(不循环)
executorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()
+ "-->" + LocalDateTime.now());
}
},5,TimeUnit.SECONDS);//推迟5秒执行
//2.scheduleAtFixedRate 推迟执行(initialDelay=0,不推迟),然后周期性循环执行task
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()
+ "-->" + LocalDateTime.now());
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
}
},5,10,TimeUnit.SECONDS);//推迟5秒执行,然后每10秒执行一次
//3.scheduleWithFixedDelay 推迟执行(initialDelay=0,不推迟),然后周期性循环执行task
executorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()
+ "-->" + LocalDateTime.now());
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
}
},5,10,TimeUnit.SECONDS);
}
}
2.1 executorService.schedule
- 创建并执行在给定延迟后启用的单次操作。
- 效果图:
- 这个没啥可说的了,上面的内容看了的,这个很好理解的,我们主要说一下 scheduleAtFixedRate 与 scheduleWithFixedDelay 的区别。
2.2 scheduleAtFixedRate 与 scheduleWithFixedDelay 区别
- 来我们,先看官方怎么说的
- 这是什么意思呢,我们看看下面的效果就非常明白了
2.2.1 executorService.scheduleAtFixedRate
- 效果图:
- 上面大概明白怎么走的了吧,区别到底在哪里,先别急,看完下面的效果就明白了!
2.2.2 executorService.scheduleWithFixedDelay
- 效果图
- 现在明白二者的区别了吧
2.2.3 小节
- scheduleAtFixedRate:是以固定频率来执行线程任务,固定频率的含义就是可能设定的固定间隔时间不足以完成线程任务,但是它不管,达到设定的延迟时间了就要开始执行下一次任务了。
- scheduleWithFixedDelay:不管线程任务的执行时间的长短,每次都要把任务执行完成后再延迟固定时间(设置的间隔时间)后再执行下一次的任务。
3. 基于多线程的定时器(SpringBoot)
3.1 提前了解 cron表达式和@Async异步
- 看它之前我们先了解一下 cron 表达式:
cron表达式的详细介绍(各域说明以及举例说明). - 还需要了解几个注解
@Async 、 @EnableAsync、 @Scheduled、@EnableScheduling
,我们下面会用到,对这个几个注解不太了解的话,这两个注解(@Async 、 @EnableAsync
)可以看下面这篇文章:
SpringBoot 自定义线程池以及多线程间的异步调用(@Async、@EnableAsync).
3.2 @EnableScheduling 与 @Scheduled
- 关于 @EnableScheduling :
在SpringBoot项目中,在启动类或者定时任务类上添加 @EnableScheduling 注解来开启对定时任务的支持。 - 关于 @Scheduled :
如果在定时任务类中的方法上添加 @Scheduled 注解,则该方法是声明需要执行的定时任务。 - @Scheduled 注解有几种类型参数,先截图下来,我们下面会简单根据例子介绍
3.3 @Scheduled 定时任务例子
- 首先启动类上添加注解
@EnableScheduling
,下面不再提醒了。
3.3.1 关于 cron 表达式的
- cron 表达式不是很清楚的,看我们上面提供的链接,此处不做解释了。
3.3.1.1 cron 例1–>单个任务
- 用注解
@Scheduled(cron = "0/5 * * * * ?")
(每5秒执行一次),代码很简单,直接截图: - 启动项目后,后台自动按cron表达式配置的执行任务,直接看运行效果:
(1)简单分析@Schedule默认定时任务的线程
- 观察上面的执行结果,不难发现,Spring 的 Schedule 定时任务默认是单线程的。而且是默认所有定时任务都是在一个线程中执行(下面我们有多任务的例子)!
- 这样的话,我们看到的只是代码层次的定时任务,而实际执行得过程是一个任务的开启需要等上一个任务的结束才行。尤其是当系统中有特别耗时的定时任务和执行频繁的定时任务,执行频繁的任务需要等耗时的任务执行完才能执行,你说这算什么定时任务!
- 会造成严重得后果就是如果我们有多个定时任务,一个卡死,其他全挂,嗯,就这样。
- 问题分析出来了,解决是不是就有了,对,异步多线程!
3.3.1.2 cron 例2–>多个任务(非异步)
- 用异步前,我们不妨来写两个定时任务看看效果,先看两个耗时都短的任务:
很明显如果耗时短还不好观察,任务1是按定时5秒执行一次,任务2也按定时2秒执行一次,没看出来,但是能看出来是单线程的执行两个任务!。 - 再来看一个耗时长,一个频繁执行的定时任务,我们让任务1睡一下就行了。
这样对比效果就很明显了吧,这明显不是我们开发中想要的实现方式,所以考虑异步,请往下…
3.3.1.3 cron 例3–>多个任务(异步@Async)
- 把上面的例子改成异步试试,很简单啥也不用,直接异步方法上加注解
@Async
,启动类加@EnableAsync
注解即可。
再说一下,这两个注解不太了解的,看下面文章,此处不做解释了。
SpringBoot 自定义线程池以及多线程间的异步调用(@Async、@EnableAsync). - 修改后的代码如下(在例子3.3.1.2上修改):
- 效果如图:
可以看到在耗时任务1执行的过程中并不影响任务2定时任务的执行,也不影响任务1自己的定时任务,什么意思,意思就是你可以简单理解为同我们上述2.2.1中介绍的executorService.scheduleAtFixedRate
,即以固定频率来执行线程任务,就是在我们任务1中,设定的固定间隔时间5秒,而5秒不足以完成线程任务(需要10秒完成),但是它不管,达到设定的间隔时间5秒后就要开始执行下一次的任务1了(即:开启新的线程来执行)。
3.3.1.4 cron 例4–>多个任务(异步@Async——自定义线程池)
- 3.3.1.3 中,虽然实现了异步,但是 @Async 也存在一个问题,就是默认线程池的问题,这个在这里不做介绍,上面链接的文章里已经说的很清楚,不明白的先了解一下 @Async 。
- 所以,我们用自定义线程池的方式实现异步定时任务
- 代码如下(文字代码上面链接介绍异步的文章里都有,这里截图一下看看,明白逻辑就行了):
- 效果如下:
3.3.1.5 从配置文件读取 cron表达式 以及停掉 cron任务
- 用配置文件的形式,主要方便我们以后更改任务的执行时间等。
- 代码如图:
(1)cron 例5–>用符号 “ - ”控制停止定时任务
注意.properties 与.yml的区别写法
- ① application.properties 中的 cron 表达式不能用双引号引起,而.yml中可以用双引号引起;
- ② 在配置文件里配置让线程任务停掉,不是简单的注释掉,而是用减号符号 “-”,需注意的是两中配置文件写法不一样,yml中必须要加双引号(因为 - 在yml中是一个特殊的字符),application.properties 必须不能用双引号,这点需要注意。
(2)cron 例6–>手动控制定时任务的开启和停止
a. 分析 @EnableScheduling
- 配置之前,我们先来分析一下源码,为什么启动类上加上
@EnableScheduling
注解就开启了对定时任务的支持了呢?好奇是吧,好奇就点呗,点进去看看…
你如果感兴趣的话,可以继续往后点,自己欣赏源码也是打发时间的一种方式,哈哈哈哈!
快烦了吧,说这么多什么意思呢?意思就是你可以不用这个注解@EnableScheduling
让定时任务跑起来。怎么跑,先听解释: - 其实,任务方法上用的
@Scheduled
注解,是被一个叫做ScheduledAnnotationBeanPostProcessor
(上面最后一张截图里出现的)的类所拦截的,所以,根据源码的实现,我们也可以根据配置,决定是否创建这个 bean,如果有这个 bean ,定时任务正常执行,如果没有这个 bean,@Scheduled 就不会被拦截,那么定时任务肯定不会执行了了,嗯,就是这个道理。请往下看怎么实现:
a. 配置文件设置定时任务的开关
- 首先,启动类上的注解 @EnableScheduling 注释掉。
- 然后,配置文件添加开关,如图:
- 然后,接下来我们说两种方式控制这个bean的创建
- 方式一:接口
Condition
与注解Conditional
结合
两种方式说完,我们再测试效果 - 方式二:直接用注解
@ConditionalOnProperty
- 来,看一下效果吧,定时任务还是那两个任务
ok了,就说这么多吧!
3.3.2 其他类型参数——简单说
- 如果目录里的 1 和 2 都看明白了,关于 @Scheduled 注解的其他参数真的没什么可说的了,比葫芦画瓢的事了,我们就简单举个例子就行了
4. springboot动态定时任务的实现
- 非动态,简单开关控制关于SpringBoot中定时任务@EnableScheduling的开关设置(针对指定的定时任务可控制)
- 动态读取配置的定时任务SpringBoot定时任务的动态配置处理(动态获取数据库配置的定时任务).
5. 附项目工程代码
- clone代码在下面的链接 由于省事,没有单独创建项目,代码就写在了之前的一个项目里,本次所有的代码(包括异步)都在如图所示的文件夹里:
- Csdn上项目代码:Java多线程之定时任务 以及 SpringBoot多线程实现定时任务.
- 码云上项目代码:https://gitee.com/liuersusu/springboot_jpa.
- GitHub 上项目代码:https://github.com/liuersusu/springboot_jpa.