我们在搭建系统的时候,不可避免地要用到定时任务
,所谓定时任务,就是到指定的时间就执行指定任务
。现实中我们比较常用到的定时任务是闹钟
:每天定时叫醒我们
,而这个叫醒我们
就是用户给闹钟设置的定时任务啦。
这里我就不再赘述了,大家参考以下这篇文章SpringBoot定时任务及Cron表达式详解
虽然默认
定时任务很方便,也可以实现一些简单
的需求,但是如果定时任务较多
的时候,就需要自己多配置一些信息了,比如线程池
。
我在使用过程中,就遇到了以下两个问题,需要注意的是以下图中的线程
是指的定时任务的线程
,默认只有一
个:
默认使用单线程
的方式来执行定时任务。
也就是任务1、2、3之中只能有任一一个在执行
,而无法同时
进行。
比如在任务1任务2都是从0秒开始运行,那么在0秒的时候本来预期是两者一起运行
,也就是以下这样:
但由于默认是单线程
,所以它就可能变成了以下两种情况:
先执行任务1在执行任务2
或者相反
同时执行的任务,在单线程的情况下随机执行一个
。
所以你可以看到单线程对于多个任务而言,有很多不方便的地方。比如某个任务耗费时间比较长
,那么其他的定时任务就必须要等它执行结束才能继续执行
。又或者有的任务直接卡住了
,导致所有的定时任务都无法执行。
多线程
的定时任务中,每个任务只分配到一个线程
。在上面的讨论中,我们注意到单线程的弊端
,那么自然有解决办法。那就是多线程执行定时任务
,多线程执行任务就是以下这样:
但大家有没有发现一个问题,就是单个线程能应付得了单个任务
吗?
举个简单的例子:我们上面的提到过的耗费时间比较长
的问题。
比如我们任务1
从0
秒开始每5秒
执行一次,但是其任务执行时间要7
秒。那么就可能导致什么问题?大家可以想一下。
3
2
1
但其实是这样:
单线程应付不了单个任务
的时候,就可能导致我们的任务并不会按照我们的预期
去执行。
如上面那样由于上一次任务的执行时间大于我的定时间隔时间
,导致我漏掉下一次任务,又或者某次执行挂掉卡住了
导致后续定时任务无法执行
。
怎么解决上述的问题?
其实这个问题,可以具体化到如何让任务的每次执行互不影响
?
3
2
1
答案揭晓:多线程
。
为任务的每次执行分配一个线程
,使得每次任务的执行两两独立
,互不影响。也就是一下这样:
下面我们就来在项目中验证
上述中提到的相关问题,请注意下面的验证顺序与上面提到的问题顺序的略有不同
。
以下内容仅仅验证
上面的叙述在SpringBoot
环境中的定时任务是否准确。
当任务的执行时间,大于你设定的时间
时,单线程、单个定时任务
如何执行?
以上面提到过的任务一的执行举例:从0秒开始每5秒钟执行一次
,但是你的任务每次执行时间需要7秒
,请设想一下在一分钟内它最终
执行多少次?
第一次执行完是7秒
,那么此时已经过了5秒的那个点,所以下一次到10秒
才会执行。也就是对应秒数到达0 ,10 ,20,30,40,50的时候才会执行,结束时间为7,17,27,37,47,57
。总共执行6次
。也就是上图中的
来看以下定时任务代码:
//从0开始 每五秒执行一次
@Scheduled(cron ="0/5 * * * * *")
public void testTread4() throws InterruptedException {
//在一分钟内执行
if(DateUtil.formatStr(new Date()).compareTo("2023-05-16 17:06") >= 0
&& DateUtil.formatStr(new Date()).compareTo("2023-05-16 17:07") < 0) {
//睡眠7秒钟
Thread.sleep(7000);
//执行结束时间
System.out.println("执行任务" + DateUtil.formatStr(new Date()));
}
}
执行结果(注意:这里的输出时间为定时任务执行结束时间而非开始时间
):
很明显符合我们预期的结果,也就是单线程
情况下,如果在执行任务期间还要执行新的任务
,那么该新任务就会被忽略
。
在单线程执行多任务
情况下,如果某个任务执行过久
,由于它是单线程
,所以会导致后续任务也被要等待它执行结束,才能继续执行
。也就是上图中的
来看以下这段代码的逻辑
//一秒执行一次
@Scheduled(cron ="0/1 * * * * *")
public void testTread2() {
System.out.println("定时任务1 开始 "+ DateUtil.formatStr(new Date()));
//假设以下是它需要执行的任务内容 以下代码执行时间超过一秒
for(int i =0; i < 1000000 * 500;i++) {
String str = "diuhfiahiudhfadfjkakhfkjahsjcndjakdjfoiejalkdjfialdjcxkjhaoifkdhjafhdkahjfkdahfaldkjfianvnjvhdhuiehrkahkjdfakhuidhfakjhducian";
String result1 = "";
result1 = result1 + str.indexOf(i % 125);
}
System.out.println("定时任务1 结束 "+ DateUtil.formatStr(new Date()));
}
//一秒执行一次
@Scheduled(cron ="0/1 * * * * *")
public void testTread3() {
System.out.println("定时任务2 开始 "+ DateUtil.formatStr(new Date()));
//假设以下是它需要执行的任务内容 常规时间执行是两秒左右
for(int i =0; i < 1000000 * 50;i++) {
String str = "diuhfiahiudhfadfjkakhfkjahsjcndjakdjfoiejalkdjfialdjcxkjhaoifkdhjafhdkahjfkdahfaldkjfianvnjvhdhuiehrkahkjdfakhuidhfakjhducian";
String result1 = "";
result1 = result1 + str.indexOf(i % 125);
}
System.out.println("定时任务2 结束 "+ DateUtil.formatStr(new Date()));
}
我们来看下结果:
可以明显看到,当有任务1未执行完
时,即使任务2到达执行的时间
,也没有执行
。
那么,如何让单线程定时任务变成多线程
呢?
画外音:忙碌了一整天的鱼师傅
回到家中,发现了这个问题,最复杂的问题往往只需要最朴素的解决办法,他打开了csdn
…
不好意思,串台了。
这里需要分为两个方面:
简单来讲,单线程多任务
的情况就是,一个
定时任务需要多个
线程来执行。
多任务多线程
就是,多
个定时任务需要多个
线程来执行。
将任务的每次执行交给一个新的线程
,任务的每次
执行独立
。
为每个任务里面的执行内容新建一个子线程
。
修改上述代码:
@Scheduled(cron ="0/5 * * * * *")
public void testTread4() {
//为每一个子任务创建一个新的线程执行 也可以将具体的任务内容封装到一个继承Thread或者实现Runnable接口的类中
new Thread(new Runnable() {
@Override
public void run() {
try {
//在一分钟内执行
if(DateUtil.formatStr(new Date()).compareTo("2023-05-16 17:25") >= 0
&& DateUtil.formatStr(new Date()).compareTo("2023-05-16 17:26") < 0) {
//睡眠7秒钟
Thread.sleep(7000);
System.out.println("执行任务" + DateUtil.formatStr(new Date()));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
执行结果
//0秒开始执行 执行7秒 07结束
执行任务2023-05-16 17:25:07
//5秒开始执行 12结束
执行任务2023-05-16 17:25:12
//10秒开始执行 17结束
执行任务2023-05-16 17:25:17
执行任务2023-05-16 17:25:22
执行任务2023-05-16 17:25:27
执行任务2023-05-16 17:25:32
执行任务2023-05-16 17:25:37
执行任务2023-05-16 17:25:42
执行任务2023-05-16 17:25:47
执行任务2023-05-16 17:25:52
执行任务2023-05-16 17:25:57
//25:55秒开始执行 26:02结束
执行任务2023-05-16 17:26:02
可以看到执行了12
次,并且每个任务的开始时间是00 - 05 - 10 - 15 - .....-55
,互不影响,两两独立。
这个逻辑只需要让每个定时任务各自拥有一个线程
,每个定时任务
之间独立
,你干你的,我干我的,互不影响。
一种方法是添加配置定时任务的配置类
,修改默认的线程池容量(默认为1,也就是单线程)
。
新建ScheduleConfig.java
配置文件,内容如下:
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Component;
@Component
public class ScheduleConfig {
@Bean
public TaskScheduler taskScheduler() {
//定时任务线程池
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
// 设置线程池大小为10
taskScheduler.setPoolSize(10);
return taskScheduler;
}
}
执行结果:
可以看到任务1和任务2同时开始执行
,不再出现谁等谁的情况了。
另一种方法是实现任务调度配置接口
– SchedulingConfigurer
,根据任务数量配置线程池钟线程最大数量
,也可以达到上面的效果。
@Component
@Configuration
@EnableScheduling
public class MySchedule
//需要实现这个接口
implements SchedulingConfigurer {
//配置定时任务线程池信息 有多少定时任务就配置多大的线程池 任务中新建的子线程并不由是此线程池里的对象
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
//获取所有方法
Method[] methods = MySchedule.class.getMethods();
//线程池默认大小10
int defaultPoolSize = 10;
//实际大小为0
int corePoolSize = 0;
Assert.notNull(methods , "并无定时任务");
if (methods.length > 0) {
//遍历所有任务 统计所有带定时任务注解的方法个数
for (Method method : methods) {
Scheduled annotation = method.getAnnotation(Scheduled.class);
if (annotation != null) {
corePoolSize++;
}
}
//最小为10
if (defaultPoolSize > corePoolSize)
corePoolSize = defaultPoolSize;
}
System.out.println("线程池大小:" + corePoolSize);
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(corePoolSize));
}
//一秒执行一次
@Scheduled(cron ="0/1 * * * * *")
public void testTread2() {
System.out.println("定时任务1 开始 "+ DateUtil.formatStr(new Date() , DateUtil.MillisecondPattern));
//假设以下是它需要执行的任务内容 以下代码执行时间超过一秒
for(int i =0; i < 1000000 * 500;i++) {
String str = "diuhfiahiudhfadfjkakhfkjahsjcndjakdjfoiejalkdjfialdjcxkjhaoifkdhjafhdkahjfkdahfaldkjfianvnjvhdhuiehrkahkjdfakhuidhfakjhducian";
String result1 = "";
result1 = result1 + str.indexOf(i % 125);
}
System.out.println("定时任务1 结束 "+ DateUtil.formatStr(new Date() , DateUtil.MillisecondPattern));
}
//一秒执行一次
@Scheduled(cron ="0/1 * * * * *")
public void testTread3() {
System.out.println("定时任务2 开始 "+ DateUtil.formatStr(new Date() , DateUtil.MillisecondPattern));
//假设以下是它需要执行的任务内容 常规时间执行是两秒左右
for(int i =0; i < 1000000 * 50;i++) {
String str = "diuhfiahiudhfadfjkakhfkjahsjcndjakdjfoiejalkdjfialdjcxkjhaoifkdhjafhdkahjfkdahfaldkjfianvnjvhdhuiehrkahkjdfakhuidhfakjhducian";
String result1 = "";
result1 = result1 + str.indexOf(i % 125);
}
System.out.println("定时任务2 结束 "+ DateUtil.formatStr(new Date() , DateUtil.MillisecondPattern));
}
}
执行结果
可以看到两个任务你干你的 我干我的
,不再出现,我要等你干完,我才能干
的情况。
需要注意的是,这里设定的池是全局通用
的,任何定时任务
都可以用这个池,而且共用一个。如果存在多个执行定时任务的类
,并且每个类中都有
上述的构建池的操作,那么会以最后执行的设置池的容量
为准。(这个大家可以自己测试一下,输出的时候加上Thread.currentThread().getName()
这个 – 获取线程名称,用于区分线程池里的线程即可。)
比如我有两个定时任务类,第一个里面有2
个定时任务,第二个里面有18
个定时任务:
那么执行的时候MySchedule
设置的池大小为10
,West01
设置的池大小为18
。最后各个任务执行的时候,所用到的池大小为18
。
如果将West01
更改为Aest01
,那么最后执行用到的池就是MySchedule
里面设置的池容量,大小为10
。
因为按照名称排序并且加载
,MySchedule
的加载排在Aest01
之后,West01
之前,容量以最后的设置
为准。
MySchedule
与West01
MySchedule
与Aest01
如果你所有的定时任务都在一个类
里面,毫无疑问第二种动态线程池
就是最佳选择。
但是你有多个定时任务类
,选择第一种
完全是ok的,但计算估计有点麻烦需要计算一下,设置为多少合理。
其实在Spring 中有一个这样的注解,专门为某些操作从线程池(可以指定 也可以使用默认)里分配了一个线程去执行,这个注解就是:@Async
。
@Async
注解用于表示一个方法是异步执行的
。该注解通常用于多线程和并发编程
的场景,它能够将一个方法标记为异步执行,并将其放入一个独立的线程中执行
,从而提高系统的并发能力和响应性能。
要使用@Async
注解,需要在Spring配置中启用异步执行的支持
,通常通过在配置类或配置文件
中添加@EnableAsync注解
来实现。另外,异步方法必须定义在一个Spring管理的组件
中,以便注解生效。
在异步方法中,还可以使用@Async注解的一些可选参数,例如executor
用于指定线程池
,value
用于指定自定义的异步方法名称等。
如以下这样:
@Component //标记为组件
@EnableScheduling //启用定时任务
@EnableAsync //启用异步注解
@Async //该类中的所有方法 异步执行 默认该类下所有方法都异步 默认线程池的最大数量与系统最大支持线程数有关 如果cpu支持超线程 那么最大数量就是cpu的两倍 否则就是cpu的个数
public class MyScheduledTask1 {
Logger logger = LoggerFactory.getLogger(MyScheduledTask1.class);
@Scheduled(cron = "0/1 * * * * *")
public void test() throws InterruptedException {
Thread.sleep(5000);//该任务可以占用多个线程
logger.info(Thread.currentThread().getName() + " 任务1 - 第一个");
}
@Scheduled(cron = "0/1 * * * * *")
public void test1() throws InterruptedException {
Thread.sleep(5000);//该任务可以占用多个线程
System.out.println(Thread.currentThread().getName() + " 任务1 - 第二个");
}
@Scheduled(cron = "0/1 * * * * *")
public void test11() throws InterruptedException {
Thread.sleep(5000);//该任务可以占用多个线程
System.out.println(Thread.currentThread().getName() + " 任务1 - - 第三个");
}
}
可以理解为,每个定时任务可以占用多个线程
。也就是一个任务可以由多个线程执行,每个任务都是如此,简而言之就是 多对多
。
在文中,我们从简单的原理描述
,到验证
,再到最后实操
;
从默认的单线程执行任务
到多线程执行单任务
,再到多线程执行多任务
。
想必你会觉得“多线程这么爽那还用什么单线程
?”但其实多线程的问题也不少,简单对比一下:
少
,多线程占用资源多
。在多线程
情况下,如果某些任务执行时间过长
,而新的任务又在不断地产生新的进程
,将会迅速耗尽计算机资源
。单线程不需要考虑资源竞争,多线程需要对共享资源做对应的处理
。这个比较容易理解。比如买票,有两个售票员卖票,现在只剩下一张票,这时他们同时接到顾客,都告诉顾客有票
,但实际操作的时候手速快的那个把票拿走了,那么后者就没有票可卖了。日志记录简单、清楚
,多线程日志记录混乱
。前者是按顺序说话,后者是这里来一句,那里来一句。还有一个就是多线程真的能解决多线程问题吗?我的看法是未必
,在多线程
实际的应用场景中,有时候本来只该执行一次
的任务,它给你执行了两次或者多次
。
比如以下是某个任务的日志记录信息,在2毫秒内3和6线程,分别执行了一次任务,而那个任务本来只应该执行一次。
就说到这个里了,还是那句话,没有最好的解决方法,只有最合适的解决方法
。
Java 多线程编程基础(详细)
Springboot @Scheduled定时任务单线程执行问题