Java——定时任务

Java——定时任务 定时任务1、Timer和TimerTask1.1、基本用法1.2、基本示例1.3、基本原理1.4、死循环1.5、异常任务1.6、总结2、ScheduledExecutorService2.1、基本用法2.2、基本示例2.3、基本原理在Java中主要有两种方式实现定时任务使用java.util包中的Timer和TimerTask。使用Java并发包中的ScheduledExecutorService。1、Timer和TimerTask1.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{TimertimernewTimer();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{TimertimernewTimer();timer.schedule(newLongRunningTask(),10);timer.schedule(newFixedDelayTask(),100,1000);}}有两个定时任务第一个运行一次但耗时5秒第二个是重复执行1秒一次第一个先运行。运行该程序会发现第二个任务只有在第一个任务运行结束后才会开始运行运行后1秒一次。如果替换上面的代码为固定频率即变为代码所示。publicclassTimerFixedRate{staticclassLongRunningTaskextendsTimerTask{//省略与代码清单18-4一样}staticclassFixedRateTaskextendsTimerTask{//省略与代码清单18-4一样}publicstaticvoidmain(String[]args)throwsInterruptedException{TimertimernewTimer();timer.schedule(newLongRunningTask(),10);timer.scheduleAtFixedRate(newFixedRateTask(),100,1000);}}运行该程序第二个任务同样只有在第一个任务运行结束后才会运行但它会把之前没有运行的次数补过来一下子运行5次输出类似下面这样long running finished 1489467662330 1489467662330 1489467662330 1489467662330 1489467662330 14894676624191.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{TimertimernewTimer();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{TimertimernewTimer();timer.schedule(newTaskA(),1,1000);timer.schedule(newTaskB(),2000,1000);}}期望TaskA每秒执行一次但TaskB会抛出异常导致整个定时任务被取消程序终止屏幕输出为taskAtaskAtaskBExceptionin threadTimer-0java.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后运行callablepublicVScheduledFutureVschedule(CallableVcallable,longdelay,TimeUnitunit);//固定频率重复执行publicScheduledFuture?scheduleAtFixedRate(Runnablecommand,longinitialDelay,longperiod,TimeUnitunit);//固定延时重复执行publicScheduledFuture?scheduleWithFixedDelay(Runnablecommand,longinitialDelay,longdelay,TimeUnitunit);}它们的返回类型都是ScheduledFuture它是一个接口扩展了Future和Delayed没有定义额外方法。这些方法的大部分语义与Timer中的基本是类似的。对于固定频率的任务第一次执行时间为initialDelay后第二次为initialDelayperiod第三次为initial-Delay2*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{ScheduledExecutorServicetimerExecutors.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{ScheduledExecutorServicetimerExecutors.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基本是类似的都有一个基于堆的优先级队列保存待执行的定时任务它的主要不同是它的背后是线程池可以有多个线程执行任务。它在任务执行后再设置下次执行的时间对于固定延时的任务更为合理。任务执行线程会捕获任务执行过程中的所有异常一个定时任务的异常不会影响其他定时任务不过发生异常的任务即使是一个重复任务不会再被调度。