当前位置:首页 > 文章列表 > 文章 > java教程 > Java定时任务教程:ScheduledExecutorService详解

Java定时任务教程:ScheduledExecutorService详解

2025-09-28 23:30:27 0浏览 收藏

“纵有疾风来,人生不言弃”,这句话送给正在学习文章的朋友们,也希望在阅读本文《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中ScheduledExecutorService定时任务的使用,说白了,就是Java提供的一个非常强大的工具,用来安排任务在未来的某个时间点执行,或者周期性地重复执行。它比老旧的Timer类要灵活、健壮得多,特别是在处理并发和异常方面,简直是现代Java应用里定时任务的首选。我个人觉得,如果你需要做定时任务,无论是简单的延时执行,还是复杂的周期性调度,ScheduledExecutorService几乎都能完美胜任。

解决方案

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

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

  1. 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(); // 通常在应用生命周期结束时调用
  2. 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();
    }
  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任务(通过scheduleAtFixedRatescheduleWithFixedDelay提交的)在执行过程中抛出了一个未捕获的运行时异常,那么这个任务的后续所有调度都会被默默地取消掉。是的,你没听错,它就停止了,而且默认情况下你可能都不知道。这在生产环境里,可能会导致一些关键的定时任务“失踪”,后果很严重。

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

最直接、最有效的策略就是:在你的任务代码内部,一定要做好异常捕获。把所有可能抛出异常的代码块都用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<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中synchronizedMap线程安全实现方法
下一篇
Java中synchronizedMap线程安全实现方法
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    499次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    484次学习
查看更多
AI推荐
  • AI 试衣:潮际好麦,电商营销素材一键生成
    潮际好麦-AI试衣
    潮际好麦 AI 试衣平台,助力电商营销、设计领域,提供静态试衣图、动态试衣视频等全方位服务,高效打造高质量商品展示素材。
    107次使用
  • 蝉妈妈AI:国内首个电商垂直大模型,抖音增长智能助手
    蝉妈妈AI
    蝉妈妈AI是国内首个聚焦电商领域的垂直大模型应用,深度融合独家电商数据库与DeepSeek-R1大模型。作为电商人专属智能助手,它重构电商运营全链路,助力抖音等内容电商商家实现数据分析、策略生成、内容创作与效果优化,平均提升GMV 230%,是您降本增效、抢占增长先机的关键。
    235次使用
  • 社媒分析AI:数说Social Research,用AI读懂社媒,驱动增长
    数说Social Research-社媒分析AI Agent
    数说Social Research是数说故事旗下社媒智能研究平台,依托AI Social Power,提供全域社媒数据采集、垂直大模型分析及行业场景化应用,助力品牌实现“数据-洞察-决策”全链路支持。
    169次使用
  • 先见AI:企业级商业智能平台,数据驱动科学决策
    先见AI
    先见AI,北京先智先行旗下企业级商业智能平台,依托先知大模型,构建全链路智能分析体系,助力政企客户实现数据驱动的科学决策。
    166次使用
  • 职优简历:AI驱动的免费在线简历制作平台,提升求职成功率
    职优简历
    职优简历是一款AI辅助的在线简历制作平台,聚焦求职场景,提供免费、易用、专业的简历制作服务。通过Markdown技术和AI功能,帮助求职者高效制作专业简历,提升求职竞争力。支持多格式导出,满足不同场景需求。
    159次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码