Java定时任务教程:ScheduledExecutorService详解
哈喽!今天心血来潮给大家带来了《Java ScheduledExecutorService定时任务教程》,想必大家应该对文章都不陌生吧,那么阅读本文就都不会很困难,以下内容主要涉及到,若是你正在学习文章,千万别错过这篇文章~希望能帮助到你!
ScheduledExecutorService是Java中用于执行定时或周期性任务的首选工具,相比Timer更灵活、健壮。它基于线程池机制,支持并发执行任务,避免单线程导致的任务阻塞和异常崩溃问题。通过Executors工厂可创建单线程或线程池实例,核心调度方法包括:schedule()用于延迟执行一次任务;scheduleAtFixedRate()按固定频率周期执行,从任务开始时间计时;scheduleWithFixedDelay()则在任务结束后等待指定延迟再执行下一次,适用于需稳定间隔的场景。对于有返回值的任务,可使用Callable配合schedule()获取Future结果。关键优势在于异常隔离——单个任务异常不会影响其他任务调度,但周期性任务若未捕获异常会导致后续调度被取消,因此必须在任务内部使用try-catch处理异常。为增强容错,可自定义ThreadFactory并设置UncaughtExceptionHandler作为兜底。生命周期管理至关重要,应调用shutdown()停止接收新任务,并结合awaitTermination()等待任务完成;若超时,则调用shutdownNow()尝试中断正在运行的任务。完整关闭流程需兼顾优雅停机与强制终止,确保资源释放,防止程序无法退出。总之,ScheduledExecutorService在调度能力、并发支持和错误处理上全面优于Timer,是现代Java应用中定时任务的最佳选择。

Java中ScheduledExecutorService定时任务的使用,说白了,就是Java提供的一个非常强大的工具,用来安排任务在未来的某个时间点执行,或者周期性地重复执行。它比老旧的Timer类要灵活、健壮得多,特别是在处理并发和异常方面,简直是现代Java应用里定时任务的首选。我个人觉得,如果你需要做定时任务,无论是简单的延时执行,还是复杂的周期性调度,ScheduledExecutorService几乎都能完美胜任。
解决方案
使用ScheduledExecutorService来管理定时任务,核心在于它的调度能力和线程池机制。我们通常会通过Executors工厂类来创建它的实例,比如newSingleThreadScheduledExecutor()(单个线程执行所有任务)或者newScheduledThreadPool(int corePoolSize)(一个线程池来执行任务,更适合并发场景)。
创建好实例之后,就可以用它提供的几种调度方法了:
schedule(Runnable command, long delay, TimeUnit unit): 这个最简单,就是让一个任务(Runnable)在指定的delay时间后执行一次。比如,你希望某个操作在用户点击后5秒才真正生效,就可以用这个。ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.schedule(() -> { System.out.println("这个任务在延迟5秒后执行了一次。"); }, 5, TimeUnit.SECONDS); // 记得关闭,否则程序可能不会退出 // scheduler.shutdown(); // 通常在应用生命周期结束时调用schedule(Callable: 和上面类似,只不过这次可以提交一个callable, long delay, TimeUnit unit) Callable任务,它能返回一个结果(通过Future对象)。如果你需要定时执行一个有返回值的操作,这个就很方便。ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); Future<String> future = scheduler.schedule(() -> { System.out.println("这个带返回值的任务在延迟3秒后执行。"); return "任务完成!"; }, 3, TimeUnit.SECONDS); try { System.out.println("任务结果: " + future.get()); // 阻塞直到任务完成并获取结果 } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); }scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit): 这个方法用于周期性地执行任务,它会严格按照固定的“速率”来调度。什么意思呢?就是从任务的“开始时间”算起,每隔period时间就尝试执行一次。如果你的任务执行时间比period短,那没问题;但如果任务执行时间很长,超过了period,那么下一个任务的执行会紧接着上一个任务结束之后立即开始,但总的调度频率依然会努力保持在period。这对于需要保持固定频率执行的任务(比如每分钟检查一次库存)非常有用。ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); // 使用线程池 System.out.println("开始执行 scheduleAtFixedRate 任务,当前时间:" + System.currentTimeMillis()); scheduler.scheduleAtFixedRate(() -> { long startTime = System.currentTimeMillis(); System.out.println("scheduleAtFixedRate 任务执行开始,时间:" + startTime); try { Thread.sleep(2000); // 模拟任务执行2秒 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("scheduleAtFixedRate 任务执行结束,耗时:" + (System.currentTimeMillis() - startTime) + "ms"); }, 1, 3, TimeUnit.SECONDS); // 首次延迟1秒,之后每3秒执行一次scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit): 这个方法也是周期性执行,但它的调度方式是“固定延迟”。也就是说,它会在当前任务执行结束后,再等待delay时间,然后才开始下一次任务。这确保了任务之间总有一个固定的间隔,不会因为任务执行时间长而导致任务堆积。对于需要确保任务之间有充分休息时间(比如,处理完一批数据后,休息一会儿再处理下一批)的场景,这个就很合适。ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); System.out.println("开始执行 scheduleWithFixedDelay 任务,当前时间:" + System.currentTimeMillis()); scheduler.scheduleWithFixedDelay(() -> { long startTime = System.currentTimeMillis(); System.out.println("scheduleWithFixedDelay 任务执行开始,时间:" + startTime); try { Thread.sleep(2000); // 模拟任务执行2秒 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("scheduleWithFixedDelay 任务执行结束,耗时:" + (System.currentTimeMillis() - startTime) + "ms"); }, 1, 3, TimeUnit.SECONDS); // 首次延迟1秒,任务执行结束后再延迟3秒开始下一次在实际应用中,别忘了在程序退出或者不再需要定时任务时,调用
scheduler.shutdown()来关闭ScheduledExecutorService,释放资源。否则,它内部的线程池会一直运行,可能导致程序无法正常退出。
ScheduledExecutorService与Timer:我该选择哪个来执行定时任务?
这个问题其实挺经典的,尤其是在一些老项目中,你可能会看到java.util.Timer的身影。但如果现在让我选,答案几乎是压倒性的:无脑选ScheduledExecutorService。
Timer这个东西,它有几个比较致命的缺点。首先,它内部只有一个线程来执行所有的定时任务。这意味着如果其中一个任务执行时间过长,它就会阻塞住其他所有等待执行的任务。更糟糕的是,如果某个任务抛出了一个未捕获的运行时异常,那么这个Timer的内部线程就会悄无声息地挂掉,导致后续所有任务都无法再执行,而且你可能还很难发现。这简直是个隐形炸弹。
相比之下,ScheduledExecutorService是基于Executor框架构建的,它天生就支持线程池。你可以配置一个核心线程数,让多个任务并发执行,互不影响。即使某个任务抛出异常,也只会影响到它自己,其他任务依然能够正常调度和执行。而且,ScheduledExecutorService提供了更完善的异常处理机制,例如你可以通过Future来获取任务执行结果和异常,或者在Runnable内部做更细致的异常捕获。在我看来,ScheduledExecutorService在健壮性、灵活性和并发处理能力上,都完胜Timer。所以,别犹豫了,新项目直接用ScheduledExecutorService,老项目如果有可能,也尽量迁移过去吧。
处理ScheduledExecutorService中任务异常的策略与实践
在使用ScheduledExecutorService进行周期性任务调度时,任务中出现异常是一个非常常见的场景。但这里有个坑,很多人可能不清楚:如果一个周期性执行的Runnable任务(通过scheduleAtFixedRate或scheduleWithFixedDelay提交的)在执行过程中抛出了一个未捕获的运行时异常,那么这个任务的后续所有调度都会被默默地取消掉。是的,你没听错,它就停止了,而且默认情况下你可能都不知道。这在生产环境里,可能会导致一些关键的定时任务“失踪”,后果很严重。
那么,怎么处理这个问题呢?
最直接、最有效的策略就是:在你的任务代码内部,一定要做好异常捕获。把所有可能抛出异常的代码块都用try-catch包起来。
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
try {
// 这里放你实际的任务逻辑
System.out.println("任务开始执行,当前时间: " + System.currentTimeMillis());
if (Math.random() > 0.7) { // 模拟偶尔出现异常
throw new RuntimeException("模拟任务执行失败!");
}
System.out.println("任务成功完成。");
} catch (Exception e) {
// 捕获所有可能的异常,并进行适当的处理,比如记录日志
System.err.println("定时任务执行异常: " + e.getMessage());
// 这里可以根据业务需求进行恢复操作,或者发送告警
}
}, 0, 5, TimeUnit.SECONDS); // 每5秒执行一次通过这种方式,即使任务内部出现异常,异常也会被捕获并处理,而不会“冒泡”到ScheduledExecutorService的调度线程,从而保证任务的周期性调度不会中断。
另外,如果你想更全面地处理线程池中所有线程的未捕获异常(不仅仅是定时任务),你可以考虑为ScheduledExecutorService提供一个自定义的ThreadFactory,并在其中设置UncaughtExceptionHandler。
ThreadFactory threadFactory = new ThreadFactory() {
private final AtomicInteger counter = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "ScheduledTask-" + counter.incrementAndGet());
t.setUncaughtExceptionHandler((thread, e) -> {
System.err.println("线程 [" + thread.getName() + "] 发生未捕获异常: " + e.getMessage());
// 这里可以做更高级的错误处理,比如重启服务或者发送通知
});
return t;
}
};
ScheduledExecutorService schedulerWithHandler = Executors.newScheduledThreadPool(1, threadFactory);
schedulerWithHandler.scheduleAtFixedRate(() -> {
System.out.println("任务执行中...");
if (Math.random() > 0.8) {
throw new RuntimeException("这个异常会被UncaughtExceptionHandler捕获!");
}
}, 0, 3, TimeUnit.SECONDS);需要注意的是,即使有了UncaughtExceptionHandler,如果周期性任务内部的Runnable抛出异常,那个任务的后续调度依然会停止。UncaughtExceptionHandler更多的是提供一个“兜底”机制,用于处理那些你确实没有预料到或者无法在try-catch中处理的极端情况。所以,核心还是那句话:在任务内部做好异常捕获是王道。
如何优雅地关闭ScheduledExecutorService并管理其生命周期?
管理ScheduledExecutorService的生命周期,尤其是如何优雅地关闭它,是一个非常重要的环节,不然很容易造成资源泄露或者程序无法正常退出。我见过不少应用因为没有正确关闭线程池,导致服务重启时端口被占用,或者内存持续增长的问题。
优雅地关闭ScheduledExecutorService通常遵循一个“先柔后刚”的原则:
shutdown(): 这是你关闭ScheduledExecutorService的第一步。调用shutdown()之后,线程池将不再接受新的任务提交,但会继续执行所有已经提交的(包括正在运行的和等待执行的)任务。它不会强制中断正在执行的任务。ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); // 提交一些任务... // ... scheduler.shutdown(); // 启动关闭序列
awaitTermination(long timeout, TimeUnit unit):shutdown()只是发出了一个关闭信号,但它不会等待任务真正完成。如果你希望在所有任务执行完毕或者等待一段时间后才继续执行主线程,那么就需要用到awaitTermination()。这个方法会阻塞当前线程,直到所有任务都执行完毕,或者指定的timeout时间已到,或者当前线程被中断。它会返回一个布尔值,表示是否所有任务都在超时前完成。try { // 等待所有任务在60秒内完成 if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) { System.err.println("线程池未能在指定时间内关闭。"); // 此时可以考虑强制关闭 } else { System.out.println("线程池已优雅关闭。"); } } catch (InterruptedException e) { // 当前线程在等待过程中被中断 System.err.println("等待线程池关闭时被中断。"); Thread.currentThread().interrupt(); // 重新设置中断标志 }shutdownNow(): 如果awaitTermination()超时了,或者你需要在紧急情况下立即停止所有任务,那么可以使用shutdownNow()。这个方法会尝试停止所有正在执行的任务(通过中断它们),并返回所有尚未开始执行的任务列表。这是一种比较激进的关闭方式,因为它可能会导致正在执行的任务中断,从而留下不一致的状态。因此,通常只有在无法优雅关闭时才作为最后的手段。// 假设在awaitTermination超时后 List<Runnable> unexecutedTasks = scheduler.shutdownNow(); System.err.println("强制关闭线程池,有 " + unexecutedTasks.size() + " 个任务未执行。"); // 对未执行的任务进行处理,比如记录日志或重新安排
一个完整的关闭流程通常是这样的:
public void shutdownScheduler(ScheduledExecutorService scheduler) {
scheduler.shutdown(); // 1. 发出关闭信号
try {
// 2. 等待一段时间,看任务能否自然完成
if (!scheduler.awaitTermination(30, TimeUnit.SECONDS)) {
System.err.println("定时任务线程池未在30秒内关闭,尝试强制关闭...");
// 3. 如果超时,强制关闭
scheduler.shutdownNow();
// 4. 再次等待,确保强制关闭成功
if (!scheduler.awaitTermination(30, TimeUnit.SECONDS)) {
System.err.println("定时任务线程池未能完全关闭。");
} else {
System.out.println("定时任务线程池已强制关闭。");
}
} else {
System.out.println("定时任务线程池已优雅关闭。");
}
} catch (InterruptedException ie) {
// 5. 如果当前线程在等待过程中被中断,也要强制关闭
System.err.println("关闭定时任务线程池时当前线程被中断,强制关闭...");
scheduler.shutdownNow();
Thread.currentThread().interrupt(); // 重新设置中断标志
}
}在使用shutdownNow()时,要注意你的任务是否能够响应中断。如果任务内部有长时间运行的阻塞操作(比如Thread.sleep()、wait()、join()或者IO操作),它们通常会抛出InterruptedException,你可以在catch块中处理中断信号,从而让任务提前结束。但如果任务是计算密集型的,不检查中断标志,那么shutdownNow()可能也无法立即停止它。所以,设计任务时,考虑其可中断性是很重要的。
今天关于《Java定时任务教程:ScheduledExecutorService详解》的内容就介绍到这里了,是不是学起来一目了然!想要了解更多关于的内容请关注golang学习网公众号!
点击链接颜色变化设置教程
- 上一篇
- 点击链接颜色变化设置教程
- 下一篇
- 小红书千帆私信回复教程详解
-
- 文章 · java教程 | 6小时前 |
- Java集合高效存储技巧分享
- 164浏览 收藏
-
- 文章 · java教程 | 6小时前 |
- JavaOpenAPI字段命名配置全攻略
- 341浏览 收藏
-
- 文章 · java教程 | 7小时前 |
- Java接口定义与实现全解析
- 125浏览 收藏
-
- 文章 · java教程 | 7小时前 |
- Java对象与线程内存交互全解析
- 427浏览 收藏
-
- 文章 · java教程 | 7小时前 |
- JPA枚举过滤技巧与实践方法
- 152浏览 收藏
-
- 文章 · java教程 | 7小时前 |
- Java获取线程名称和ID的技巧
- 129浏览 收藏
-
- 文章 · java教程 | 7小时前 |
- JavanCopies生成重复集合技巧
- 334浏览 收藏
-
- 文章 · java教程 | 7小时前 |
- Windows配置Gradle环境变量方法
- 431浏览 收藏
-
- 文章 · java教程 | 8小时前 |
- Java合并两个Map的高效技巧分享
- 294浏览 收藏
-
- 文章 · java教程 | 8小时前 | java class属性 Class实例 getClass() Class.forName()
- Java获取Class对象的4种方式
- 292浏览 收藏
-
- 文章 · java教程 | 8小时前 |
- Java正则表达式:字符串匹配与替换技巧
- 183浏览 收藏
-
- 文章 · java教程 | 8小时前 |
- Java处理外部接口异常的正确方法
- 288浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ChatExcel酷表
- ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
- 3182次使用
-
- Any绘本
- 探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
- 3393次使用
-
- 可赞AI
- 可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
- 3424次使用
-
- 星月写作
- 星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
- 4528次使用
-
- MagicLight
- MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
- 3802次使用
-
- 提升Java功能开发效率的有力工具:微服务架构
- 2023-10-06 501浏览
-
- 掌握Java海康SDK二次开发的必备技巧
- 2023-10-01 501浏览
-
- 如何使用java实现桶排序算法
- 2023-10-03 501浏览
-
- Java开发实战经验:如何优化开发逻辑
- 2023-10-31 501浏览
-
- 如何使用Java中的Math.max()方法比较两个数的大小?
- 2023-11-18 501浏览

