Java:jdk实现定时任务的两种方式

299 阅读9分钟

    定时任务是每个业务常见的需求,比如每分钟扫描超时支付的订单,每小时清理一次数据库历史数据,每天统计前一天的数据并生成报表等等。

    Java自带的Thread线程类、Timer、ScheduledExecutorService都可以用于实现定时任务。以下对后两种方式进行介绍。

方式1:使用Timer实现定时任务

Timer类简介

    java的java.util包下提供了Timer类,可用于指定作为定时任务执行,还是一次性任务执行。jdk8源码中对应的文档注释内容如下,基本含义为:


        Timer类可实现在后台线程中完成子线程调度,每一个子线程可以执行指定的任务内容。这些任务可以是一次性的任务、或者是定时任务。
        一个Timer对象对应一个后台线程,这个Timer对象的任务就是负责依次执行该对象所拥有的task任务。Timer定时器任务应当快速执行完毕,否则可能会影响后续任务的执行(换言之:当添加并执行多个任务时,前面任务的执行用时和异常将影响到后面任务,这也是Timer实现定时任务方式的缺点所在)。
        Timer定时任务的正常结束条件:当对Timer定时器对象的最后一个引用断开,并且所有的任务都已经执行完毕。此时,Timer对象就可以被垃圾回收器回收,但是,这个回收时间是不确定的。默认情况下,Timer——定时任务执行线程并不作为守护线程执行,因此它能够使得应用程序终止。因此,如果开发者想要快速终止定时任务执行线程(Timer对象所对应的线程),可以主动调用Timer类的cancel()方法。
        如果Timer定时任务执行线程异常终止,例如:由于Timer对象的`stop()`方法被调用,那么所有对于定时任务的调度操作都会导致IllegalStateException异常,就像定时器的`cancel`方法被调用一样。
        Timer是线程安全的,即:多个线程可以共享同一个Timer对象,并且不需要做任何线程同步(synchronized)操作。
        但是,Timer类不提供是实时监控保证的,因为它的内部通过`Object.wait(long)`方法调度任务。
        Timer定时任务调度类,可以扩展到大量并发计划任务(千应该没有问题)。 在内部,它使用二进制堆表示其任务队列,因此计划任务的成本为O(log n),其中n为并发计划任务的数量。

 * background thread.  Tasks may be scheduled for one-time execution, or for
 * repeated execution at regular intervals.
 *
 * <p>Corresponding to each <tt>Timer</tt> object is a single background
 * thread that is used to execute all of the timer's tasks, sequentially.
 * Timer tasks should complete quickly.  If a timer task takes excessive time
 * to complete, it "hogs" the timer's task execution thread.  This can, in
 * turn, delay the execution of subsequent tasks, which may "bunch up" and
 * execute in rapid succession when (and if) the offending task finally
 * completes.
 *
 * <p>After the last live reference to a <tt>Timer</tt> object goes away
 * <i>and</i> all outstanding tasks have completed execution, the timer's task
 * execution thread terminates gracefully (and becomes subject to garbage
 * collection).  However, this can take arbitrarily long to occur.  By
 * default, the task execution thread does not run as a <i>daemon thread</i>,
 * so it is capable of keeping an application from terminating.  If a caller
 * wants to terminate a timer's task execution thread rapidly, the caller
 * should invoke the timer's <tt>cancel</tt> method.
 *
 * <p>If the timer's task execution thread terminates unexpectedly, for
 * example, because its <tt>stop</tt> method is invoked, any further
 * attempt to schedule a task on the timer will result in an
 * <tt>IllegalStateException</tt>, as if the timer's <tt>cancel</tt>
 * method had been invoked.
 *
 * <p>This class is thread-safe: multiple threads can share a single
 * <tt>Timer</tt> object without the need for external synchronization.
 *
 * <p>This class does <i>not</i> offer real-time guarantees: it schedules
 * tasks using the <tt>Object.wait(long)</tt> method.
 *
 * <p>Implementation note: This class scales to large numbers of concurrently
 * scheduled tasks (thousands should present no problem).  Internally,
 * it uses a binary heap to represent its task queue, so the cost to schedule
 * a task is O(log n), where n is the number of concurrently scheduled tasks.
 *
 * <p>Implementation note: All constructors start a timer thread.

Timer类:基本方法

构造方法

    通过构造方法,可以指定Timer任务调度线程的执行方法(是否以守护线程的方式执行,前面提到:默认情况下,Timer对应的是非守护线程),也可以指定Timer对象的线程名城。

image.png

成员方法

    Timer类的成员方法如下,比较重要的有:

1cancel():取消附属于当前Timer对象的所有定时任务的执行;
【2schedule():传入TimerTask对象,指定定时任务的执行。

image.png

※关于schedule和scheduleAtFixedRate两个方法的区别

    简单来讲,scheduleAtFixedRate()方法更注重定时任务的执行频率,即:如果以scheduleAtFixedRate()方法指定的TimeTask定时任务A,那么,在执行期间如果由于其它任务执行时间较长,而导致定时任务A错过了两个执行周期,那么,在其它任务执行完毕之后,定时任务A就会立即执行(2+1=3)次。
    而schedule()方法指定的定时任务则不会,它只会按部就班的逐次执行。     相关解析在jdk原码的文档注释中也给出了,如下所示,

     * Schedules the specified task for repeated <i>fixed-rate execution</i>,
     * beginning at the specified time. Subsequent executions take place at
     * approximately regular intervals, separated by the specified period.
     *
     * <p>In fixed-rate execution, each execution is scheduled relative to the
     * scheduled execution time of the initial execution.  If an execution is
     * delayed for any reason (such as garbage collection or other background
     * activity), two or more executions will occur in rapid succession to
     * "catch up."  In the long run, the frequency of execution will be
     * exactly the reciprocal of the specified period (assuming the system
     * clock underlying <tt>Object.wait(long)</tt> is accurate).  As a
     * consequence of the above, if the scheduled first time is in the past,
     * then any "missed" executions will be scheduled for immediate "catch up"
     * execution.
     *
     * <p>Fixed-rate execution is appropriate for recurring activities that
     * are sensitive to <i>absolute</i> time, such as ringing a chime every
     * hour on the hour, or running scheduled maintenance every day at a
     * particular time.  It is also appropriate for recurring activities
     * where the total time to perform a fixed number of executions is
     * important, such as a countdown timer that ticks once every second for
     * ten seconds.  Finally, fixed-rate execution is appropriate for
     * scheduling multiple repeating timer tasks that must remain synchronized
     * with respect to one another.
     *
     * @param task   task to be scheduled.
     * @param firstTime First time at which task is to be executed.
     * @param period time in milliseconds between successive task executions.
     * @throws IllegalArgumentException if {@code firstTime.getTime() < 0} or
     *         {@code period <= 0}
     * @throws IllegalStateException if task was already scheduled or
     *         cancelled, timer was cancelled, or timer thread terminated.
     * @throws NullPointerException if {@code task} or {@code firstTime} is null
     */
    public void scheduleAtFixedRate(TimerTask task, Date firstTime,
                                    long period) {
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, firstTime.getTime(), period);
    }

Timer类:实现定时任务

    以下,我们使用Timer定时任务调度类实现定时任务,示例代码如下,

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class Timer_Test {
    //properties

    //methods
    public static void main(String[] args) throws ParseException {
        //创建Timer对象
        Timer timer = new Timer("Timer tasks schedule instance");
        //执行定时任务
        //1-延迟3秒执行任务
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("TimerTask-1");
            }
        },1000*3);

        //2-指定线程任务在何时被一次性执行
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date parse = simpleDateFormat.parse("2022-12-04 22:45:00");
        System.out.println(parse);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定时任务在 "+simpleDateFormat.format(parse)+" 被执行");
            }
        },parse);

        //3-从指定时间开始,以固定延迟时间3s持续执行
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定时任务在 "+simpleDateFormat.format(parse)+" 被执行[延迟执行]");
            }
        },parse,1000*3);

        //4-从指定时间开始,以固定延迟时间3s持续执行
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println("[scheduleAtFixedRate]定时任务在 "+simpleDateFormat.format(parse)+" 被执行[延迟执行]");
            }
        },parse,1000*3);
    }
}

    执行效果如下,程序一旦启动之后就会进入阻塞状态,从而在指定周期结束时去执行指定的任务。

image.png

TimerTask类

    TimerTask类是Runnable接口的子类,用于表示可以由计时器进行一次性或重复执行的任务。其内部维护了lock锁对象,以线程同步的方式来保证任务的安全执行。

image.png     其构造函数和成员方法如下,

image.png     在使用构造函数创建TimerTask对象时,需要重写run()方法,如下,

        Timer timer = new Timer("Timer tasks schedule instance");
        //执行定时任务
        //1-延迟3秒执行任务
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("TimerTask-1");
            }
        },1000*3);

方式2:使用ScheduledExecutorService实现定时任务

    在jdk8-Timer类的源码文档注释中,也提供了如下的内容,主要含义为:         Java 5.0引入了java.util.concurrent并发编程工具包,以及并发工具java.util.concurrent.ScheduledThreadPoolExecutor,该类表示一个线程池,用于定期或者延迟执行任务。ScheduleThreadPoolExecutor类是Timer+TimerTask组合实现定时任务的替代品,因为该类允许多个服务线程,接受各种时间单位,并且不需要子类TimerTask (只实现Runnable )。所以,只需要配置一个ScheduleThreadPoolExecutor类就等同于Timer类。

 * one of the concurrency utilities therein is the {@link
 * java.util.concurrent.ScheduledThreadPoolExecutor
 * ScheduledThreadPoolExecutor} which is a thread pool for repeatedly
 * executing tasks at a given rate or delay.  It is effectively a more
 * versatile replacement for the {@code Timer}/{@code TimerTask}
 * combination, as it allows multiple service threads, accepts various
 * time units, and doesn't require subclassing {@code TimerTask} (just
 * implement {@code Runnable}).  Configuring {@code
 * ScheduledThreadPoolExecutor} with one thread makes it equivalent to
 * {@code Timer}.

ScheduledExecutorService接口介绍

image.png
    ScheduledExecutorService接口继承了ExecutorService类(ExecutorService类是jdk内置的一个线程池类,它可以有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞,同时提供定时执行、定期执行、单线程、并发数控制等功能),jdk8的源码中文档注释内容如下,主要含义为:
        ExecutorService可以在给定延迟时间之后,或者周期性的调度(线程执行)命令。
        ScheduledExecutorService接口中的schedule()方法可以创建任务,并给定不同的延迟时间;并且将创建好的任务对象返回;通过这个对象可以实现任务的取消或者检查执行。
        ScheduledExecutorService接口中的scheduleAtFixedRate()方法和scheduleWithFixedDelay()方法可创建一个周期性执行的任务,直到该任务被主动取消。
        使用Executorexecute(Runnable runnable)方法、ExecutorServicesubmit()方法提交的命令以零延迟的方式进行调度。并且,零和负数的延迟时间也被schedule()方法所允许,并且被视为立即执行的任务。
        所有的schedule()方法接收一个相对延迟和时间跨度作为参数,而非绝对时间或者日期。因此,在指定延迟时间时,可以采用方式schedule(task, date.getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS)进行指定。
        Executors类为此包中提供的ScheduledExecutorService实现提供了方便的工厂方法。

 * An {@link ExecutorService} that can schedule commands to run after a given
 * delay, or to execute periodically.
 *
 * <p>The {@code schedule} methods create tasks with various delays
 * and return a task object that can be used to cancel or check
 * execution. The {@code scheduleAtFixedRate} and
 * {@code scheduleWithFixedDelay} methods create and execute tasks
 * that run periodically until cancelled.
 *
 * <p>Commands submitted using the {@link Executor#execute(Runnable)}
 * and {@link ExecutorService} {@code submit} methods are scheduled
 * with a requested delay of zero. Zero and negative delays (but not
 * periods) are also allowed in {@code schedule} methods, and are
 * treated as requests for immediate execution.
 *
 * <p>All {@code schedule} methods accept <em>relative</em> delays and
 * periods as arguments, not absolute times or dates. It is a simple
 * matter to transform an absolute time represented as a {@link
 * java.util.Date} to the required form. For example, to schedule at
 * a certain future {@code date}, you can use: {@code schedule(task,
 * date.getTime() - System.currentTimeMillis(),
 * TimeUnit.MILLISECONDS)}. Beware however that expiration of a
 * relative delay need not coincide with the current {@code Date} at
 * which the task is enabled due to network time synchronization
 * protocols, clock drift, or other factors.
 *
 * <p>The {@link Executors} class provides convenient factory methods for
 * the ScheduledExecutorService implementations provided in this package.
 *
 * <h3>Usage Example</h3>
 *
 * Here is a class with a method that sets up a ScheduledExecutorService
 * to beep every ten seconds for an hour:
 *
 *  <pre> {@code
 * import static java.util.concurrent.TimeUnit.*;
 * class BeeperControl {
 *   private final ScheduledExecutorService scheduler =
 *     Executors.newScheduledThreadPool(1);
 *
 *   public void beepForAnHour() {
 *     final Runnable beeper = new Runnable() {
 *       public void run() { System.out.println("beep"); }
 *     };
 *     final ScheduledFuture<?> beeperHandle =
 *       scheduler.scheduleAtFixedRate(beeper, 10, 10, SECONDS);
 *     scheduler.schedule(new Runnable() {
 *       public void run() { beeperHandle.cancel(true); }
 *     }, 60 * 60, SECONDS);
 *   }
 * }}</pre>
 *
 * @since 1.5
 * @author Doug Lea
 */

ScheduledExecutorService接口方法

    ScheduledExecutorService接口包含四个方法,主要用法在上面一节已经提及。其中:

    -ScheduledExecutorService接口中的schedule()方法可以创建指定延迟时间执行的任务;并且将创建好的任务对象返回;通过这个对象可以实现任务的取消或者检查执行。
    -ScheduledExecutorService接口中的scheduleAtFixedRate()方法和scheduleWithFixedDelay()方法可创建一个周期性执行的任务,直到该任务被主动取消。两者之间的区别如下图所示,

image.png

image.png

ScheduledExecutorService接口使用示例

    jdk8源码的文档注释中也给出了ScheduledExecutorService接口的使用示例,代码如下,

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledExecutorService_Test {
    //properties

    //methods
    public static void main(String[] args) throws ParseException {
        //日期格式化类对象
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date parse = simpleDateFormat.parse("2022-12-04 23:53:30");
        long initDelay = (long) ((parse.getTime() - System.currentTimeMillis())/1000.0);
        System.out.println("initDelay="+initDelay);

        //使用Executors工具类提供的方法创建核心线程容量为5的ScheduledExecutorService接口对象
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
        //创建Runnable接口对象
        Runnable runnable_fixedRate = ()->{
            System.out.println("[runnable]定时任务延迟3s执行...");
        };
        Runnable runnable_regularly = ()->{
            System.out.println("[runnable]定时任务周期性执行-【首次执行时间:2022-12-04 23:47:30】...");
        };

        //配置定时任务
        //1指定延迟3秒执行定时任务
        executorService.schedule(runnable_fixedRate,3, TimeUnit.SECONDS);
        //2-指定周期性执行的定时任务-[配置:首次执行的时间;首次执行之后周期性执行的延迟时间]---不关心其它任务执行的耗时,不会累计时间
        executorService.scheduleAtFixedRate(runnable_regularly,initDelay,3,TimeUnit.SECONDS);

    }
}

image.png