0

0

Java中ScheduledExecutorService定时任务使用

P粉602998670

P粉602998670

发布时间:2025-09-20 18:53:01

|

178人浏览过

|

来源于php中文网

原创

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

java中scheduledexecutorservice定时任务使用

Java中

ScheduledExecutorService
定时任务的使用,说白了,就是Java提供的一个非常强大的工具,用来安排任务在未来的某个时间点执行,或者周期性地重复执行。它比老旧的
Timer
类要灵活、健壮得多,特别是在处理并发和异常方面,简直是现代Java应用里定时任务的首选。我个人觉得,如果你需要做定时任务,无论是简单的延时执行,还是复杂的周期性调度,
ScheduledExecutorService
几乎都能完美胜任。

解决方案

使用

ScheduledExecutorService
来管理定时任务,核心在于它的调度能力和线程池机制。我们通常会通过
Executors
工厂类来创建它的实例,比如
newSingleThreadScheduledExecutor()
(单个线程执行所有任务)或者
newScheduledThreadPool(int corePoolSize)
(一个线程池来执行任务,更适合并发场景)。

创建好实例之后,就可以用它提供的几种调度方法了:

  1. schedule(Runnable command, long delay, TimeUnit unit)
    : 这个最简单,就是让一个任务(
    Runnable
    )在指定的
    delay
    时间后执行一次。比如,你希望某个操作在用户点击后5秒才真正生效,就可以用这个。

    立即学习Java免费学习笔记(深入)”;

    ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    scheduler.schedule(() -> {
        System.out.println("这个任务在延迟5秒后执行了一次。");
    }, 5, TimeUnit.SECONDS);
    // 记得关闭,否则程序可能不会退出
    // scheduler.shutdown(); // 通常在应用生命周期结束时调用
  2. schedule(Callable callable, long delay, TimeUnit unit)
    : 和上面类似,只不过这次可以提交一个
    Callable
    任务,它能返回一个结果(通过
    Future
    对象)。如果你需要定时执行一个有返回值的操作,这个就很方便。

    ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    Future future = scheduler.schedule(() -> {
        System.out.println("这个带返回值的任务在延迟3秒后执行。");
        return "任务完成!";
    }, 3, TimeUnit.SECONDS);
    
    try {
        System.out.println("任务结果: " + future.get()); // 阻塞直到任务完成并获取结果
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
  3. 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秒执行一次
  4. 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
提交的)在执行过程中抛出了一个未捕获的运行时异常,那么这个任务的后续所有调度都会被默默地取消掉。是的,你没听错,它就停止了,而且默认情况下你可能都不知道。这在生产环境里,可能会导致一些关键的定时任务“失踪”,后果很严重。

jspgou网店系统
jspgou网店系统

JSPGOU一直以来都和jeecms、jeebbs一样,是收费软件,但是从2014年7月12日开始,开始jspgou发布第一个免费版,金磊科技承诺:永远不向使用jspgou免费版用户索取任何费用,为免费用户提供更好的技术支持服务,根据用户提出的完善建议快速完善jspgou系统。 jspgou系统使命:做中国最优秀的免费网店系统,让更多的用户了解和使用java产品。 jspgou免费版适用对象:不限

下载

那么,怎么处理这个问题呢?

最直接、最有效的策略就是:在你的任务代码内部,一定要做好异常捕获。把所有可能抛出异常的代码块都用

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
通常遵循一个“先柔后刚”的原则:

  1. shutdown()
    : 这是你关闭
    ScheduledExecutorService
    的第一步。调用
    shutdown()
    之后,线程池将不再接受新的任务提交,但会继续执行所有已经提交的(包括正在运行的和等待执行的)任务。它不会强制中断正在执行的任务。

    ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
    // 提交一些任务...
    // ...
    scheduler.shutdown(); // 启动关闭序列
  2. 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(); // 重新设置中断标志
    }
  3. shutdownNow()
    : 如果
    awaitTermination()
    超时了,或者你需要在紧急情况下立即停止所有任务,那么可以使用
    shutdownNow()
    。这个方法会尝试停止所有正在执行的任务(通过中断它们),并返回所有尚未开始执行的任务列表。这是一种比较激进的关闭方式,因为它可能会导致正在执行的任务中断,从而留下不一致的状态。因此,通常只有在无法优雅关闭时才作为最后的手段。

    // 假设在awaitTermination超时后
    List 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()
可能也无法立即停止它。所以,设计任务时,考虑其可中断性是很重要的。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

443

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

544

2024.08.29

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

73

2025.08.29

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

197

2025.08.29

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

395

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

575

2023.08.10

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

502

2023.08.10

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

502

2023.08.10

Python 自然语言处理(NLP)基础与实战
Python 自然语言处理(NLP)基础与实战

本专题系统讲解 Python 在自然语言处理(NLP)领域的基础方法与实战应用,涵盖文本预处理(分词、去停用词)、词性标注、命名实体识别、关键词提取、情感分析,以及常用 NLP 库(NLTK、spaCy)的核心用法。通过真实文本案例,帮助学习者掌握 使用 Python 进行文本分析与语言数据处理的完整流程,适用于内容分析、舆情监测与智能文本应用场景。

10

2026.01.27

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.9万人学习

C# 教程
C# 教程

共94课时 | 7.7万人学习

Java 教程
Java 教程

共578课时 | 52.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号