前言
首先看到标题,我们其实很多人都知道,但是呢 在实际项目中我们面对很多延迟任务实现方案有很多选择,甚至直接在网上百度反正都能实现就行,但是忽略了很多细节,导致生产上的事故,都是因为没有真正了解到底层的运行,当然这篇博文不去说源码而是说实战,源码的尽头都是线程池,这个已经在之前已经详细介绍过了
ScheduledThreadPoolExecutor和@Scheduled的关系
说到定时任务大家首先想到的是@Scheduled注解,其实这个注解的底层就是ScheduledThreadPoolExecutor
在Spring Boot中集成ScheduledThreadPoolExecutor用于执行定时任务,你可以通过以下步骤来实现:
定义线程池配置类
创建一个配置类来定义你的ScheduledThreadPoolExecutor实例。这里是一个简单的示例:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
public class ThreadPoolConfig {
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 设置线程池大小
scheduler.setThreadNamePrefix("scheduled-task-"); // 设置线程前缀
scheduler.setRemoveOnCancelPolicy(true); // 取消任务时是否移除
return scheduler;
}
}
注意,虽然上述示例中使用了ThreadPoolTaskScheduler,它是Spring对定时任务线程池的封装,底层也是基于ScheduledThreadPoolExecutor实现的,因此适合用于集成定时任务。
使用@EnableScheduling开启定时任务支持
在你的Spring Boot主类或配置类上添加@EnableScheduling注解,以启用定时任务功能。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
编写定时任务方法
在需要执行定时任务的类上使用@Component注解使其成为Spring管理的Bean,并在相应的方法上使用@Scheduled注解来定义任务的执行规则。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class MyScheduledTasks {
@Autowired
private SomeService someService;
@Scheduled(fixedRate = 60000) // 每60秒执行一次
public void scheduledTask() {
someService.doSomething();
}
}
在这个例子中,fixedRate = 60000表示方法执行的间隔时间,单位是毫秒,这里设置为每60秒执行一次。
通过上述步骤,你就可以在Spring Boot应用中集成并使用ScheduledThreadPoolExecutor来执行定时任务了。记得根据实际需求调整线程池的大小和其他参数。
切记spring中几乎所有关于定时任务线程池的封装都是通过ScheduledThreadPoolExecutor来执行的
另外这个有个陷阱那就是一定需要自定义线程池的配置类,为什么呢,看看@Scheduled引发的生产事故
@Scheduled引发的生产事故
上面的截图只是一个demo,其中一个定时任务卡死了,导致其他的定时任务无法启动,从而造成了线上故障,而这些定时器任务都是由ThreadPoolTaskScheduleduler来调度的,这个这个线程池的核心线程是1,所以一定要自定义配置类,重新设置核心线程数
或者
ScheduledThreadPoolExecutor和@Scheduled能解决什么业务场景
从刚刚上面的例子无论是ThreadPoolTaskScheduler还是@Scheduled其实底层都是ScheduledThreadPoolExecutor进行封装的,那么接下来
我们看下这个如何使用呢?
scheduledExecutorService.scheduled
等待执行时间之后,运行任务。(任务,延迟时间,延迟时间单位)
ScheduledExecutorService.schedule(Callable callable, long delay, TimeUnit unit)
主程序
创建5个任务,分别延迟1 2 3 4 5 秒执行,然后等待任务完成。
package xyz.jangle.thread.test.n4_6.schedule;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 4.6、schedule 在执行器内延迟执行任务
* 创建5个任务,分别延迟1 2 3 4 5 秒执行,然后等待任务完成。
*
* 注:如果希望在指定时间执行任务, 则计算当前到指定时间的间隔。
* @author jangle
* @email jangle@jangle.xyz
* @time 2020年8月21日 下午7:00:39
*
*/
public class M {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
/*
// ScheduledThreadPoolExecutor threadPool = (ScheduledThreadPoolExecutor) scheduledExecutorService;
// 设置false的情况下,会使得调用shutdown()后,未执行的延迟任务不再执行(默认是true,继续执行延迟的任务)
// threadPool.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
*/
System.out.println("Main:开始执行主程序" + new Date());
for (int i = 0; i < 5; i++) {
Task task = new Task("Task " + i);
// 执行task任务,延迟i+1秒执行。
scheduledExecutorService.schedule(task, i + 1, TimeUnit.SECONDS);
}
// shutdown 之后,是否继续执行延迟任务受属性executeExistingDelayedTasksAfterShutdown影响
scheduledExecutorService.shutdown();
try {
// 等待所有任务执行完毕,最长等待2天时间。
scheduledExecutorService.awaitTermination(2, TimeUnit.DAYS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main:结束执行" + new Date());
}
}
任务
package xyz.jangle.thread.test.n4_6.schedule;
import java.util.Date;
import java.util.concurrent.Callable;
/**
* 普通的任务
* @author jangle
* @email jangle@jangle.xyz
* @time 2020年8月21日 下午7:02:27
*
*/
public class Task implements Callable<String> {
private final String name;
public Task(String name) {
super();
this.name = name;
}
@Override
public String call() throws Exception {
System.out.println(this.name + "开始执行任务" + new Date());
return "hello,schedule task";
}
}
上面的代码可以优化下:
package xyz.jangle.thread.test.n4_6.schedule;
import java.time.LocalDateTime;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 示例展示如何使用ScheduledExecutorService安排延时执行的任务。
* 五个任务分别延迟1至5秒后执行,主程序等待所有任务完成。
*
* 注意:若希望在特定时间点执行任务,需计算当前时间至该时间点的时间差。
*
* @author Jangle
* @since 2020-08-21
*/
public class ScheduledTaskDemo {
public static void main(String[] args) {
// 创建单线程的ScheduledExecutorService
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
System.out.println("主程序开始执行:" + LocalDateTime.now());
// 提交五个延迟任务
for (int delay = 1; delay <= 5; delay++) {
int finalDelay = delay; // 使用final或effectively final变量在lambda表达式中
executor.schedule(() -> {
System.out.println("执行任务:Task " + finalDelay + " at " + LocalDateTime.now());
}, finalDelay, TimeUnit.SECONDS);
}
// 关闭调度器,不再接受新任务,已提交的任务将继续执行
executor.shutdown();
try {
// 等待所有任务完成,最多等待2天
if (!executor.awaitTermination(2, TimeUnit.DAYS)) {
System.err.println("等待超时,部分任务未完成。");
}
} catch (InterruptedException e) {
executor.shutdownNow(); // 中断等待,尝试取消正在执行的任务
Thread.currentThread().interrupt(); // 保持中断状态
System.err.println("主线程被中断。");
} finally {
System.out.println("主程序执行结束:" + LocalDateTime.now());
}
}
}
优化点包括:
使用LocalDateTime.now()替代new Date()以获得更易读的日期时间表示。
将类名M改为更具描述性的ScheduledTaskDemo。
注释内容更新,提供更清晰的说明。
在循环内部使用final或实际上的final变量finalDelay,以符合lambda表达式的语法要求。
在awaitTermination后增加处理逻辑,以应对等待超时或线程中断的情况,提高了代码的健壮性。
移除了注释掉的代码片段,保持代码清爽。若需要自定义ScheduledThreadPoolExecutor的行为,应显式创建并配置,而不是向下转型。
如何获取各个定时任务的返回结果呢
其实这个问题我在别的博文中已经详细介绍过了,为了让大家将之前所学的知识串起来,附上链接可以跳转
【CompletableFuture】批量异步任务处理
解决方案一:使用Future来收集结果集
如果您需要获取并打印每个定时任务的返回结果,您需要将Runnable改为Callable,以便任务能返回结果,并使用Future来接收这些结果。下面是修改后的代码示例:
package xyz.jangle.thread.test.n4_6.schedule;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class ScheduledTaskDemoWithResults {
public static void main(String[] args) {
// 创建单线程的ScheduledExecutorService
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
System.out.println("主程序开始执行:" + LocalDateTime.now());
List<Future<String>> futures = new ArrayList<>();
// 提交五个延迟任务,这些任务现在返回一个字符串结果
for (int delay = 1; delay <= 5; delay++) {
int finalDelay = delay; // 使用final或effectively final变量在lambda表达式中
Future<String> future = executor.schedule(() -> {
System.out.println("执行任务:Task " + finalDelay + " at " + LocalDateTime.now());
return "Result from Task " + finalDelay;
}, finalDelay, TimeUnit.SECONDS);
futures.add(future); // 收集Future对象
}
// 关闭调度器,不再接受新任务,已提交的任务将继续执行
executor.shutdown();
try {
// 等待所有任务完成,并收集结果
for (Future<String> future : futures) {
// get()方法会阻塞直到结果可用,这里可以使用get(long timeout, TimeUnit unit)来设定超时
String result = future.get();
System.out.println("任务结果:" + result);
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
executor.shutdownNow(); // 中断等待,尝试取消正在执行的任务
Thread.currentThread().interrupt(); // 保持中断状态
System.err.println("主线程被中断或任务执行时出现异常。");
} finally {
System.out.println("主程序执行结束:" + LocalDateTime.now());
}
}
}
解决方案二:使用CompletableFuture.allOf()来收集结果集
使用 java.util.concurrent.CompletableFuture 和 CompletableFuture.allOf() 方法是一种更现代且灵活的方式来管理异步任务,尤其是当你需要等待一组任务全部完成后再继续执行后续操作时。下面是如何在你的场景中使用 CompletableFuture 和 allOf() 的示例:
package xyz.jangle.thread.test.n4_6.schedule;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class ScheduledTaskDemoWithAllOf {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 创建单线程的ScheduledExecutorService
ExecutorService executor = Executors.newSingleThreadScheduledExecutor();
System.out.println("主程序开始执行:" + LocalDateTime.now());
List<CompletableFuture<String>> futures = new ArrayList<>();
// 提交五个延迟任务,使用CompletableFuture来代表异步操作
for (int delay = 1; delay <= 5; delay++) {
int finalDelay = delay;
CompletableFuture<String> future = CompletableFuture.supplyAsync(
() -> {
System.out.println("执行任务:Task " + finalDelay + " at " + LocalDateTime.now());
return "Result from Task " + finalDelay;
},
executor // 使用自定义的Executor
).orTimeout(2, TimeUnit.SECONDS); // 可选:为每个任务设置超时
futures.add(future);
}
// 创建一个 CompletableFuture,它将在所有输入的CompletableFuture完成后完成
CompletableFuture<Void> allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
// 当所有任务都完成时,打印所有结果
allFutures.thenAccept(v -> {
for (CompletableFuture<String> future : futures) {
try {
String result = future.get(); // 这里会立即返回,因为我们已经知道所有任务都完成了
System.out.println("任务结果:" + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
System.out.println("所有任务已完成:" + LocalDateTime.now());
});
// 关闭ExecutorService
executor.shutdown();
executor.awaitTermination(2, TimeUnit.DAYS); // 等待所有任务完成
System.out.println("主程序执行结束:" + LocalDateTime.now());
}
}
执行结果
Main:开始执行主程序Fri Aug 21 19:19:40 CST 2020
Task 0开始执行任务Fri Aug 21 19:19:41 CST 2020
Task 1开始执行任务Fri Aug 21 19:19:42 CST 2020
Task 2开始执行任务Fri Aug 21 19:19:43 CST 2020
Task 3开始执行任务Fri Aug 21 19:19:44 CST 2020
Task 4开始执行任务Fri Aug 21 19:19:45 CST 2020
Main:结束执行Fri Aug 21 19:19:45 CST 2020
总结
本篇博文的主题中心还是和大家介绍下定时任务如何使用且大家常见的用法之间的联系,各个spring底层封存的工具类有什么联系包括使用陷阱,其实解决的场景只有一个就是为了延迟处理任务,除了使用定时任务线程池之外,还有其他更好的方案【如何优雅的实现延迟消息多次提醒】方案集合