news 2026/5/17 1:03:03

Java——定时任务

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java——定时任务

定时任务

    • 1、Timer和TimerTask
      • 1.1、基本用法
      • 1.2、基本示例
      • 1.3、基本原理
      • 1.4、死循环
      • 1.5、异常任务
      • 1.6、总结
    • 2、ScheduledExecutorService
      • 2.1、基本用法
      • 2.2、基本示例
      • 2.3、基本原理

在Java中,主要有两种方式实现定时任务:

  • 使用java.util包中的Timer和TimerTask。
  • 使用Java并发包中的ScheduledExecutorService。

1、Timer和TimerTask

1.1、基本用法

TimerTask表示一个定时任务,它是一个抽象类,实现了Runnable,具体的定时任务需要继承该类,实现run方法。Timer是一个具体类,它负责定时任务的调度和执行,主要方法有:

//在指定绝对时间time运行任务taskpublicvoidschedule(TimerTasktask,Datetime)//在当前时间延时delay毫秒后运行任务taskpublicvoidschedule(TimerTasktask,longdelay)//固定延时重复执行,第一次计划执行时间为firstTime,//后一次的计划执行时间为前一次"实际"执行时间加上periodpublicvoidschedule(TimerTasktask,DatefirstTime,longperiod)//同样是固定延时重复执行,第一次执行时间为当前时间加上delaypublicvoidschedule(TimerTasktask,longdelay,longperiod)//固定频率重复执行,第一次计划执行时间为firstTime,//后一次的计划执行时间为前一次"计划"执行时间加上periodpublicvoidscheduleAtFixedRate(TimerTasktask,DatefirstTime,longperiod)//同样是固定频率重复执行,第一次计划执行时间为当前时间加上delaypublicvoidscheduleAtFixedRate(TimerTasktask,longdelay,longperiod)

需要注意固定延时(fixed-delay)与固定频率(fixed-rate)的区别,二者都是重复执行,但后一次任务执行相对的时间是不一样的,对于固定延时,它是基于上次任务的“实际”执行时间来算的,如果由于某种原因,上次任务延时了,则本次任务也会延时,而固定频率会尽量补够运行次数。

另外,需要注意的是,如果第一次计划执行的时间firstTime是一个过去的时间,则任务会立即运行,对于固定延时的任务,下次任务会基于第一次执行时间计算,而对于固定频率的任务,则会从firstTime开始算,有可能加上period后还是一个过去时间,从而连续运行很多次,直到时间超过当前时间。我们通过一些简单的例子具体来看下。

1.2、基本示例

看一个最简单的例子,如代码所示。

publicclassBasicTimer{staticclassDelayTaskextendsTimerTask{@Overridepublicvoidrun(){System.out.println("delayed task");}}publicstaticvoidmain(String[]args)throwsInterruptedException{Timertimer=newTimer();timer.schedule(newDelayTask(),1000);Thread.sleep(2000);timer.cancel();}}

创建一个Timer对象,1秒钟后运行DelayTask,最后调用Timer的cancel方法取消所有定时任务。

publicclassTimerFixedDelay{staticclassLongRunningTaskextendsTimerTask{@Overridepublicvoidrun(){try{Thread.sleep(5000);}catch(InterruptedExceptione){}System.out.println("long running finished");}}staticclassFixedDelayTaskextendsTimerTask{@Overridepublicvoidrun(){System.out.println(System.currentTimeMillis());}}publicstaticvoidmain(String[]args)throwsInterruptedException{Timertimer=newTimer();timer.schedule(newLongRunningTask(),10);timer.schedule(newFixedDelayTask(),100,1000);}}

有两个定时任务,第一个运行一次,但耗时5秒,第二个是重复执行,1秒一次,第一个先运行。运行该程序,会发现,第二个任务只有在第一个任务运行结束后才会开始运行,运行后1秒一次。如果替换上面的代码为固定频率,即变为代码所示。

publicclassTimerFixedRate{staticclassLongRunningTaskextendsTimerTask{//省略,与代码清单18-4一样}staticclassFixedRateTaskextendsTimerTask{//省略,与代码清单18-4一样}publicstaticvoidmain(String[]args)throwsInterruptedException{Timertimer=newTimer();timer.schedule(newLongRunningTask(),10);timer.scheduleAtFixedRate(newFixedRateTask(),100,1000);}}

运行该程序,第二个任务同样只有在第一个任务运行结束后才会运行,但它会把之前没有运行的次数补过来,一下子运行5次,输出类似下面这样:

long running finished 1489467662330 1489467662330 1489467662330 1489467662330 1489467662330 1489467662419

1.3、基本原理

Timer内部主要由任务队列和Timer线程两部分组成。任务队列是一个基于堆实现的优先级队列,按照下次执行的时间排优先级。Timer线程负责执行所有的定时任务,需要强调的是,一个Timer对象只有一个Timer线程,所以,对于上面的例子,任务会被延迟。

Timer线程主体是一个循环,从队列中获取任务,如果队列中有任务且计划执行时间小于等于当前时间,就执行它,如果队列中没有任务或第一个任务延时还没到,就睡眠。如果睡眠过程中队列上添加了新任务且新任务是第一个任务,Timer线程会被唤醒,重新进行检查。

在执行任务之前,Timer线程判断任务是否为周期任务,如果是,就设置下次执行的时间并添加到优先级队列中,对于固定延时的任务,下次执行时间为当前时间加上period,对于固定频率的任务,下次执行时间为上次计划执行时间加上period。

1.4、死循环

一个Timer对象只有一个Timer线程,这意味着,定时任务不能耗时太长,更不能是无限循环。看个例子,如代码所示。

publicclassEndlessLoopTimer{staticclassLoopTaskextendsTimerTask{@Overridepublicvoidrun(){while(true){try{//模拟执行任务Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}}}}//永远也没有机会执行staticclassExampleTaskextendsTimerTask{@Overridepublicvoidrun(){System.out.println("hello");}}publicstaticvoidmain(String[]args)throwsInterruptedException{Timertimer=newTimer();timer.schedule(newLoopTask(),10);timer.schedule(newExampleTask(),100);}}

第一个定时任务是一个无限循环,其后的定时任务ExampleTask将永远没有机会执行。

1.5、异常任务

关于Timer线程,还需要强调非常重要的一点:在执行任何一个任务的run方法时,一旦run抛出异常,Timer线程就会退出,从而所有定时任务都会被取消。我们看个简单的示例,如代码所示。

publicclassTimerException{staticclassTaskAextendsTimerTask{@Overridepublicvoidrun(){System.out.println("task A");}}staticclassTaskBextendsTimerTask{@Overridepublicvoidrun(){System.out.println("task B");thrownewRuntimeException();}}publicstaticvoidmain(String[]args)throwsInterruptedException{Timertimer=newTimer();timer.schedule(newTaskA(),1,1000);timer.schedule(newTaskB(),2000,1000);}}

期望TaskA每秒执行一次,但TaskB会抛出异常,导致整个定时任务被取消,程序终止,屏幕输出为:

taskAtaskAtaskBExceptionin thread"Timer-0"java.lang.RuntimeExceptionatlaoma.demo.timer.TimerException$TaskB.run(TimerException.java:21)atjava.util.TimerThread.mainLoop(Timer.java:555)atjava.util.TimerThread.run(Timer.java:505)

所以,如果希望各个定时任务不互相干扰,一定要在run方法内捕获所有异常。

1.6、总结

可以看到,Timer/TimerTask的基本使用是比较简单的,但我们需要注意:

  • 后台只有一个线程在运行;
  • 固定频率的任务被延迟后,可能会立即执行多次,将次数补够;
  • 固定延时任务的延时相对的是任务执行前的时间
  • 不要在定时任务中使用无限循环;
  • 一个定时任务的未处理异常会导致所有定时任务被取消。

2、ScheduledExecutorService

由于Timer/TimerTask的一些问题,Java并发包引入了ScheduledExecutorService,下面我们介绍它的基本用法、基本示例和基本原理。

2.1、基本用法

ScheduledExecutorService是一个接口,其定义为:

publicinterfaceScheduledExecutorServiceextendsExecutorService{//单次执行,在指定延时delay后运行commandpublicScheduledFuture<?>schedule(Runnablecommand,longdelay,TimeUnitunit);//单次执行,在指定延时delay后运行callablepublic<V>ScheduledFuture<V>schedule(Callable<V>callable,longdelay,TimeUnitunit);//固定频率重复执行publicScheduledFuture<?>scheduleAtFixedRate(Runnablecommand,longinitialDelay,longperiod,TimeUnitunit);//固定延时重复执行publicScheduledFuture<?>scheduleWithFixedDelay(Runnablecommand,longinitialDelay,longdelay,TimeUnitunit);}

它们的返回类型都是ScheduledFuture,它是一个接口,扩展了Future和Delayed,没有定义额外方法。这些方法的大部分语义与Timer中的基本是类似的。对于固定频率的任务,第一次执行时间为initialDelay后,第二次为initialDelay+period,第三次为initial-Delay+2*period,以此类推。不过,对于固定延时的任务,它是从任务执行后开始算的,第一次为initialDelay后,第二次为第一次任务执行结束后再加上delay。与Timer不同,它不支持以绝对时间作为首次运行的时间。

ScheduledExecutorService的主要实现类是ScheduledThreadPoolExecutor,它是线程池ThreadPoolExecutor的子类,是基于线程池实现的,它的主要构造方法是:

publicScheduledThreadPoolExecutor(intcorePoolSize)

此外,还有构造方法可以接受参数ThreadFactory和RejectedExecutionHandler,含义与ThreadPoolExecutor一样,我们就不赘述了。

它的任务队列是一个无界的优先级队列,所以最大线程数对它没有作用,即使core-PoolSize设为0,它也会至少运行一个线程。

工厂类Executors也提供了一些方便的方法,以方便创建ScheduledThreadPoolExecutor,如下所示:

//单线程的定时任务执行服务publicstaticScheduledExecutorServicenewSingleThreadScheduledExecutor()publicstaticScheduledExecutorServicenewSingleThreadScheduledExecutor(ThreadFactorythreadFactory)//多线程的定时任务执行服务publicstaticScheduledExecutorServicenewScheduledThreadPool(intcorePoolSize)publicstaticScheduledExecutorServicenewScheduledThreadPool(intcorePoolSize,ThreadFactorythreadFactory)

2.2、基本示例

由于可以有多个线程执行定时任务,一般任务就不会被某个长时间运行的任务所延迟了。比如,对于TimerFixedDelay,如果改为代码所示:

publicclassScheduledFixedDelay{staticclassLongRunningTaskimplementsRunnable{//省略,与代码清单18-4一样}staticclassFixedDelayTaskimplementsRunnable{//省略,与代码清单18-4一样}publicstaticvoidmain(String[]args)throwsInterruptedException{ScheduledExecutorServicetimer=Executors.newScheduledThreadPool(10);timer.schedule(newLongRunningTask(),10,TimeUnit.MILLISECONDS);timer.scheduleWithFixedDelay(newFixedDelayTask(),100,1000,TimeUnit.MILLISECONDS);}}

再次执行,第二个任务就不会被第一个任务延迟了。

另外,与Timer不同,单个定时任务的异常不会再导致整个定时任务被取消,即使后台只有一个线程执行任务。我们看个例子,如代码所示。

publicclassScheduledException{staticclassTaskAimplementsRunnable{@Overridepublicvoidrun(){System.out.println("task A");}}staticclassTaskBimplementsRunnable{@Overridepublicvoidrun(){System.out.println("task B");thrownewRuntimeException();}}publicstaticvoidmain(String[]args)throwsInterruptedException{ScheduledExecutorServicetimer=Executors.newSingleThreadScheduledExecutor();timer.scheduleWithFixedDelay(newTaskA(),0,1,TimeUnit.SECONDS);timer.scheduleWithFixedDelay(newTaskB(),2,1,TimeUnit.SECONDS);}}

TaskA和TaskB都是每秒执行一次,TaskB两秒后执行,但一执行就抛出异常,屏幕的输出类似如下:

taskAtaskAtaskBtaskAtaskA

这说明,定时任务TaskB被取消了,但TaskA不受影响,即使它们是由同一个线程执行的。不过,需要强调的是,与Timer不同,没有异常被抛出,TaskB的异常没有在任何地方体现。所以,与Timer中的任务类似,应该捕获所有异常。

2.3、基本原理

ScheduledThreadPoolExecutor的实现思路与Timer基本是类似的,都有一个基于堆的优先级队列,保存待执行的定时任务,它的主要不同是:

  1. 它的背后是线程池,可以有多个线程执行任务。
  2. 它在任务执行后再设置下次执行的时间,对于固定延时的任务更为合理。
  3. 任务执行线程会捕获任务执行过程中的所有异常,一个定时任务的异常不会影响其他定时任务,不过,发生异常的任务(即使是一个重复任务)不会再被调度。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/17 0:58:08

基于LLM的智能接口模拟工具ChatMock:原理、部署与实战应用

1. 项目概述&#xff1a;ChatMock&#xff0c;一个让接口模拟更智能的开发者工具如果你是一名后端开发者&#xff0c;或者经常需要和API打交道的全栈工程师&#xff0c;那么“接口模拟”这个场景你一定不陌生。无论是前端等后端接口联调&#xff0c;还是微服务之间的依赖测试&a…

作者头像 李华
网站建设 2026/5/17 0:57:11

量化交易策略记忆系统:从事件存储到智能决策回溯

1. 项目概述&#xff1a;一个专为量化交易设计的记忆系统最近在GitHub上看到一个挺有意思的项目&#xff0c;叫bsharpe/openclaw-qms-memory。光看这个名字&#xff0c;可能有点摸不着头脑&#xff0c;但如果你对量化交易、策略回测或者AI在金融领域的应用感兴趣&#xff0c;那…

作者头像 李华
网站建设 2026/5/17 0:55:38

ElevenLabs阿拉伯文语音生成失效真相(方言适配盲区大起底)

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;ElevenLabs阿拉伯文语音生成失效的表象与影响 近期大量用户反馈&#xff0c;ElevenLabs API 在处理阿拉伯语&#xff08;ar-XA、ar-SA 等区域代码&#xff09;文本时返回静音音频、空响应或 400 Bad Re…

作者头像 李华
网站建设 2026/5/17 0:55:11

基于RAG与向量数据库的代码智能理解与知识库构建实践

1. 项目概述&#xff1a;从“开源CUAK”到AI驱动的代码理解与知识库构建最近在GitHub上看到一个挺有意思的项目&#xff0c;叫open-cuak。乍一看这个名字&#xff0c;可能有点摸不着头脑&#xff0c;CUAK是什么&#xff1f;其实&#xff0c;这是“Code Understanding and Knowl…

作者头像 李华
网站建设 2026/5/17 0:49:28

OpenCV图像处理:用subtract()函数做背景差分,轻松实现运动目标检测(附Python/C++代码)

OpenCV实战&#xff1a;基于背景差分的高效运动目标检测技术解析 在智能监控、交通流量统计和人机交互等领域&#xff0c;运动目标检测一直是计算机视觉的核心课题。传统帧差法和背景差分法因其实现简单、计算高效的特点&#xff0c;至今仍在实时系统中广泛应用。本文将深入探讨…

作者头像 李华
网站建设 2026/5/17 0:48:24

协作智能体训练框架:从多智能体强化学习到自然语言通信实战

1. 项目概述&#xff1a;一个面向协作智能体的开源训练场如果你正在研究多智能体系统&#xff0c;尤其是那些需要多个AI实体通过沟通、协商、分工来完成复杂任务的场景&#xff0c;那么你很可能已经感受到了一个痛点&#xff1a;缺乏一个标准、易用且功能丰富的训练与评估环境。…

作者头像 李华