定时调度Timer与ScheduledExecutorService的使用与源码解析

    技术2023-10-23  115

    文章目录

    内容简介1.定时器Timer的使用1.1 schedule(TimerTask task, Date time) 的使用1.1.1 执行时间晚于当前时间1.1.2 执行时间早于当前时间1.1.3 多任务的执行顺序 1.2 schedule(TimerTask task, Date firstTime, long period)1.2.1 计划时间晚于当前时间1.2.2 计划时间早于当前时间1.2.3 多个TimerTask任务并延时执行1.3 TimerTask类的cancel()1.4 Timer类的cancel() 1.5 scheduleAtFixedRate(TimerTask task, Date firstTime, long period)1.5.1 scheduleAtFixedRate执行任务不延时1.5.2 scheduleAtFixedRate执行任务延时1.5.3 schedule()和scheduleAtFixedRate()的追赶性验证 2. Timer的弊端及替代品ScheduledExecutorService2.1 schedule Runnable2.2 schedule Callable 2.3 scheduleAtFixedRate2.4 scheduleWithFixedDelay2.5 scheduleAtFixedRate与scheduleWithFixedDelay的实验结论结语

    内容简介

    1.如何实现指定时间执行任务。 2.如何实现按指定周期执行任务。 3.Timer与ScheduledExecutorService的区别

    1.定时器Timer的使用

    在JDK中Timer类主要负责计划任务的功能,也就是指定的时间执行某一任务。Time类的主要作用就是设置计划任务,封装任务的类是TimerTask。 TimerTask是一个实现了Runnable接口的抽象类,代表一个可以被Timer执行的任务。

    Timer类的四个构造方法如下:

    /** * 创建一个新计时器 */ public Timer() { this("Timer-" + serialNumber()); } /** * 创建一个新计时器isDaemon=true 可以使其相关线程作为守护程序运行 * @param isDaemon true:可以使其相关线程作为守护程序运行 */ public Timer(boolean isDaemon) { this("Timer-" + serialNumber(), isDaemon); } /** * * 创建一个新计时器,并为线程指定名称 * @param name 线程名称 * @throws NullPointerException if {@code name} is null * @since 1.5 */ public Timer(String name) { thread.setName(name); thread.start(); } /** * 创建一个新的计时器,指定名称并指定是否作为守护程序运行 * * 。 * @param name 线程名称 * @param isDaemon true:可以使其相关线程作为守护程序运行 * @throws NullPointerException if {@code name} is null * @since 1.5 */ public Timer(String name, boolean isDaemon) { thread.setName(name); thread.setDaemon(isDaemon); thread.start(); }

    Timer成员方法:

    /** * 等待delay毫秒后执行且仅执行一次task * * @param task 待执行的任务 * @param delay 延迟执行时间(ms) * @throws IllegalArgumentException delay<0 || (delay + System.currentTimeMillis())<0 * @throws NullPointerException null == task */ public void schedule(TimerTask task, long delay) { if (delay < 0) throw new IllegalArgumentException("Negative delay."); sched(task, System.currentTimeMillis()+delay, 0); } /** * 在时间等于或超过time的时候执行且仅执行一次task * * @param task 待执行任务 * @param time 指定的任务开始时间 * @throws IllegalArgumentException 如果 time.getTime()< 0 || 任务已经被调度、取消 || 定时器取消或终止抛出异常 * @throws NullPointerException null == task */ public void schedule(TimerTask task, Date time) { sched(task, time.getTime(), 0); } /** * 等待delay毫秒后首次执行task, 之后每个period毫秒重复执行一次task * * @param task 待执行任务 * @param delay 延迟时间(ms)0代表无延迟 * @param period 间隔周期时间(ms) * @throws IllegalArgumentException delay<0 || (delay + System.currentTimeMillis())<0 || 任务已经被调度、取消 || 定时器取消或终止 * @throws NullPointerException if {@code task} is null */ public void schedule(TimerTask task, long delay, long period) { if (delay < 0) throw new IllegalArgumentException("Negative delay."); if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, System.currentTimeMillis()+delay, -period); } /** * 时间>=firstTime时首次执行task,之后每隔peroid毫秒重复执行一次task * * @param task 待执行任务 * @param firstTime 第一次开始时间 * @param period 间隔周期时间(ms) * @throws IllegalArgumentException firstTime.getTime() < 0<0 || period <= 0 || 任务已经被调度、取消 || 定时器取消或终止 * @throws NullPointerException null == task || null == firstTime */ public void schedule(TimerTask task, Date firstTime, long period) { if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, firstTime.getTime(), -period); } /** * 等待delay毫秒后首次执行task, 之后每个period毫秒重复执行一次task * * @param task 待执行任务 * @param delay 延迟时间(ms)0代表无延迟 * @param period 间隔周期时间(ms) * @throws IllegalArgumentException * delay < 0 || delay + System.currentTimeMillis() < 0 || period <= 0 || 任务已经被调度、取消 || 定时器取消或终止 * @throws NullPointerException null == task */ public void scheduleAtFixedRate(TimerTask task, long delay, long period) { if (delay < 0) throw new IllegalArgumentException("Negative delay."); if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, System.currentTimeMillis()+delay, period); } /** * 时间等>=time时首次执行task,之后每隔peroid毫秒重复执行一次task * * @param task 待执行任务 * @param firstTime 第一次开始时间 * @param period 间隔周期时间(ms) * @throws IllegalArgumentException * firstTime.getTime() < 0 || period <= 0 || 任务已经被调度、取消 || 定时器取消或终止 * @throws NullPointerException null == task || null == firstTime */ public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) { if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, firstTime.getTime(), period); } /** * 在指定时间执行调度任务执行,以固定时间间隔的方式重复执行,后续将一大约period毫秒的固定时间间隔执行。 * * Timer类中有多个schedule重载方法,内部都调用sched方法,它只是调用Quequ队列add,把TimerTask添加进去。Timer充当中介者的角色。 */ private void sched(TimerTask task, long time, long period) { if (time < 0) throw new IllegalArgumentException("Illegal execution time."); // 约束period,防止数值溢出 if (Math.abs(period) > (Long.MAX_VALUE >> 1)) period >>= 1; synchronized(queue) { if (!thread.newTasksMayBeScheduled) throw new IllegalStateException("Timer already cancelled."); synchronized(task.lock) { if (task.state != TimerTask.VIRGIN) throw new IllegalStateException( "Task already scheduled or cancelled"); task.nextExecutionTime = time; task.period = period; task.state = TimerTask.SCHEDULED; } queue.add(task); if (queue.getMin() == task) queue.notify(); } }

    Timer内部类TimerThread的run方法源码如下:

    public void run() { try { mainLoop(); } finally { // 线程停止 synchronized(queue) { newTasksMayBeScheduled = false; queue.clear(); // 清空队列 } } } /** * The main timer loop. (See class comment.) */ private void mainLoop() { while (true) { try { TimerTask task; boolean taskFired; synchronized(queue) { // 等待任务队列中存在任务 while (queue.isEmpty() && newTasksMayBeScheduled) queue.wait(); if (queue.isEmpty()) break; // 队列为空终止while // 队列非空获取并处理符合条件的任务 long currentTime, executionTime; task = queue.getMin(); synchronized(task.lock) { if (task.state == TimerTask.CANCELLED) { queue.removeMin(); continue; // 任务为取消状态,不做操作,再次轮询队列 } currentTime = System.currentTimeMillis(); executionTime = task.nextExecutionTime; if (taskFired = (executionTime<=currentTime)) { if (task.period == 0) { // 没有重复的,从任务队列中remove queue.removeMin(); task.state = TimerTask.EXECUTED; } else { // 重复的任务 queue.rescheduleMin( task.period<0 ? currentTime - task.period : executionTime + task.period); } } } if (!taskFired) // 任务是否可以开始 queue.wait(executionTime - currentTime); } if (taskFired) // taskFired == true 任务符合条件开始执行 task.run(); } catch(InterruptedException e) { } } }

    通过上图总而言之:new Timer()时,会创建TimerThread线程并启动,继承TimerTask重写run方法,调用Timer提供的六种执行方法之一,将TimerTask add到TaskQueue中,TimerThread的run方法调用mainloop()无线循环一个最小堆的任务队列(排序是根据 TimerTask 的 nextExecutionTime),取出最新需要执行的任务,执行TimerTask中的run方法。

    1.1 schedule(TimerTask task, Date time) 的使用

    在时间等于或超过time的时候执行且仅执行一次task

    1.1.1 执行时间晚于当前时间

    public class Test { private static Timer timer = new Timer(); public static class FirstTimerTask extends TimerTask { @Override public void run() { System.out.println("第一个timerTask,时间为:" + dateTimeFormat(new Date())); } } public static void main(String[] args) throws ParseException { FirstTimerTask firstTimerTask = new FirstTimerTask(); Date taskStartTime = dateTimeFormat("2020-07-06 11:16:59"); System.out.println("任务指定开始时间:" + dateTimeFormat(taskStartTime) + " 当前时间:" + dateTimeFormat(new Date())); timer.schedule(firstTimerTask, taskStartTime); } private static String dateTimeFormat(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } private static Date dateTimeFormat(String date) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(date); } } 任务指定开始时间:2020-07-06 11:27:59 当前时间:2020-07-06 11:26:37 第一个timerTask,时间为:2020-07-06 11:27:59

    代码虽然执行完了,但是进程却仍未销毁,还在运行中,原因是:

    public Timer() { this("Timer-" + serialNumber()); } public Timer(String name) { thread.setName(name); thread.start(); }

    如上,Timer启动的线程不是守护线程,所以会一直运行。 解决办法可以使用Timer(boolean isDaemon)或public Timer(String name, boolean isDaemon)构造方法,isDaemon=true来解决,使它当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。

    1.1.2 执行时间早于当前时间

    那么,如果执行任务的时间早于当前时间呢?执行还是不执行?执行的话,是什么时候执行呢 ?下面我们来实验一下: 修改taskStartTime时间为当前时间之前的时间:

    public class Test { private static Timer timer = new Timer(true); public static class FirstTimerTask extends TimerTask { @Override public void run() { System.out.println("第一个timerTask,时间为:" + dateTimeFormat(new Date())); } } public static void main(String[] args) throws ParseException { FirstTimerTask firstTimerTask = new FirstTimerTask(); Date taskStartTime = dateTimeFormat("2020-07-06 12:27:59"); System.out.println("任务指定开始时间:" + dateTimeFormat(taskStartTime) + " 当前时间:" + dateTimeFormat(new Date())); timer.schedule(firstTimerTask, taskStartTime); } private static String dateTimeFormat(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } private static Date dateTimeFormat(String date) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(date); } } 任务指定开始时间:2020-07-06 12:27:59 当前时间:2020-07-06 13:19:30 第一个timerTask,时间为:2020-07-06 13:19:30 Process finished with exit code 0

    结论:若执行任务时间早于当前时间,则会立即执行task任务。

    1.1.3 多任务的执行顺序

    需要注意一点的是:单任务执行isDaemon可以指定为true,当多任务执行的时候如果指定为true,可能会造成待执行的TimerTask的run()还未执行,Timer就已经随着JVM一起结束了。

    public class Test { private static Timer timer = new Timer(); public static class FirstTimerTask extends TimerTask { @Override public void run() { System.out.println("任务1开始运行,时间为:" + dateTimeFormat(new Date())); try { Thread.sleep(20000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("任务1结束运行,时间为:" + dateTimeFormat(new Date())); } } public static class SecondTimerTask extends TimerTask { @Override public void run() { System.out.println("任务2开始运行,时间为:" + dateTimeFormat(new Date())); } } public static void main(String[] args) throws ParseException { FirstTimerTask firstTimerTask = new FirstTimerTask(); Date task1StartTime = dateTimeFormat("2020-07-06 13:31:00"); System.out.println("任务1运行指定开始时间:" + dateTimeFormat(task1StartTime) + " 当前时间:" + dateTimeFormat(new Date())); SecondTimerTask secondTimerTask = new SecondTimerTask(); Date task2StartTime = dateTimeFormat("2020-07-06 13:31:00"); System.out.println("任务2运行指定开始时间:" + dateTimeFormat(task2StartTime) + " 当前时间:" + dateTimeFormat(new Date())); timer.schedule(firstTimerTask, task1StartTime); timer.schedule(secondTimerTask, task2StartTime); } private static String dateTimeFormat(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } private static Date dateTimeFormat(String date) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(date); } } 任务1运行指定开始时间:2020-07-06 13:31:00 当前时间:2020-07-06 13:30:57 任务2运行指定开始时间:2020-07-06 13:31:00 当前时间:2020-07-06 13:30:57 任务1开始运行,时间为:2020-07-06 13:31:00 任务1结束运行,时间为:2020-07-06 13:31:20 任务2开始运行,时间为:2020-07-06 13:31:20

    我们会发现,两个任务开始时间相同,但是执行的时间却不是一致的,任务2在任务1执行结束才开始执行,这是什么原因呢?因为Task是放入队列中的,需要排队顺序执行。

    1.2 schedule(TimerTask task, Date firstTime, long period)

    时间>=firstTime时首次执行task,之后每隔peroid毫秒重复执行一次task

    1.2.1 计划时间晚于当前时间

    public class Test { private static Timer timer = new Timer(); public static class Task extends TimerTask { @Override public void run() { System.out.println("运行时间为:" + dateTimeFormat(new Date())); } } public static void main(String[] args) throws ParseException { Task task = new Task(); String dateTime = "2020-07-06 17:36:00"; Date date = dateTimeFormat(dateTime); System.out.println("开始执行时间:" + dateTime + " 当前时间:" + dateTimeFormat(new Date())); timer.schedule(task, date, 4000); } private static String dateTimeFormat(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } private static Date dateTimeFormat(String date) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(date); } } 开始执行时间:2020-07-06 17:36:00 当前时间:2020-07-06 17:35:55 运行时间为:2020-07-06 17:36:00 运行时间为:2020-07-06 17:36:04 运行时间为:2020-07-06 17:36:08 运行时间为:2020-07-06 17:36:12 运行时间为:2020-07-06 17:36:16 运行时间为:2020-07-06 17:36:20 ···

    1.2.2 计划时间早于当前时间

    若果计划时间早于当前时间,立即执行task任务。

    1.2.3 多个TimerTask任务并延时执行

    public class Test { private static Timer timer = new Timer(); public static class FirstTimerTask extends TimerTask { @Override public void run() { System.out.println("任务1开始运行,时间为:" + dateTimeFormat(new Date())); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("任务1结束运行,时间为:" + dateTimeFormat(new Date())); } } public static class SecondTimerTask extends TimerTask { @Override public void run() { System.out.println("任务2开始运行,时间为:" + dateTimeFormat(new Date())); } } public static void main(String[] args) throws ParseException { FirstTimerTask firstTimerTask = new FirstTimerTask(); Date task1StartTime = dateTimeFormat("2020-07-06 18:37:40"); System.out.println("任务1运行指定开始时间:" + dateTimeFormat(task1StartTime) + " 当前时间:" + dateTimeFormat(new Date())); SecondTimerTask secondTimerTask = new SecondTimerTask(); Date task2StartTime = dateTimeFormat("2020-07-06 18:37:40"); System.out.println("任务2运行指定开始时间:" + dateTimeFormat(task2StartTime) + " 当前时间:" + dateTimeFormat(new Date())); timer.schedule(firstTimerTask, task1StartTime, 1000); timer.schedule(secondTimerTask, task2StartTime, 1000); } private static String dateTimeFormat(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } private static Date dateTimeFormat(String date) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(date); } } 任务1运行指定开始时间:2020-07-06 18:37:40 当前时间:2020-07-06 18:48:24 任务2运行指定开始时间:2020-07-06 18:37:40 当前时间:2020-07-06 18:48:24 任务1开始运行,时间为:2020-07-06 18:48:24 任务1结束运行,时间为:2020-07-06 18:48:34 任务2开始运行,时间为:2020-07-06 18:48:34 任务1开始运行,时间为:2020-07-06 18:48:34 任务1结束运行,时间为:2020-07-06 18:48:44 任务2开始运行,时间为:2020-07-06 18:48:44 任务1开始运行,时间为:2020-07-06 18:48:44 任务1结束运行,时间为:2020-07-06 18:48:54 任务2开始运行,时间为:2020-07-06 18:48:54 任务1开始运行,时间为:2020-07-06 18:48:54 任务1结束运行,时间为:2020-07-06 18:49:04 任务2开始运行,时间为:2020-07-06 18:49:04 ···

    由于firstTime<当前时间,所以当任务1立即执行,按照间隔1s的时间进行执行,但是任务1中休眠10s,我们可以看到,当任务1执行时间>period时,任务2等任务1执行完毕后立即执行,任务1随之执行。

    1.3 TimerTask类的cancel()

    /** * 任务未被安排. */ static final int VIRGIN = 0; /** * 未执行将要执行(如果非重复的任务) */ static final int SCHEDULED = 1; /** * 非重复任务已经执行或正在执行 */ static final int EXECUTED = 2; /** * 任务已取消 */ static final int CANCELLED = 3; /** * 取消定时器的任务。 * 任务为单次的并且未执行或未调度,他将不会继续运行; * 任务时重复性任务,如果调用时,任务正在执行,等任务运行完成后,不会继续运行。 * @return true 取消成功 * @return false 任务为单次执行或已经运行,或当前任务未被调度,则返回false */ public boolean cancel() { synchronized(lock) { boolean result = (state == SCHEDULED); state = CANCELLED; return result; } }

    下面我们写个例子 :

    public class Test { private static Timer timer = new Timer(); public static class FirstTimerTask extends TimerTask { @Override public void run() { System.out.println("任务1开始运行,时间为:" + dateTimeFormat(new Date())); this.cancel(); System.out.println("任务1结束运行,时间为:" + dateTimeFormat(new Date())); } } public static class SecondTimerTask extends TimerTask { @Override public void run() { System.out.println("任务2开始运行,时间为:" + dateTimeFormat(new Date())); } } public static void main(String[] args) throws ParseException { FirstTimerTask firstTimerTask = new FirstTimerTask(); Date task1StartTime = dateTimeFormat("2020-07-06 18:37:40"); System.out.println("任务1运行指定开始时间:" + dateTimeFormat(task1StartTime) + " 当前时间:" + dateTimeFormat(new Date())); SecondTimerTask secondTimerTask = new SecondTimerTask(); Date task2StartTime = dateTimeFormat("2020-07-06 18:37:40"); System.out.println("任务2运行指定开始时间:" + dateTimeFormat(task2StartTime) + " 当前时间:" + dateTimeFormat(new Date())); timer.schedule(firstTimerTask, task1StartTime, 1000); timer.schedule(secondTimerTask, task2StartTime, 1000); } private static String dateTimeFormat(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } private static Date dateTimeFormat(String date) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(date); } } 任务1运行指定开始时间:2020-07-06 18:37:40 当前时间:2020-07-06 19:06:27 任务2运行指定开始时间:2020-07-06 18:37:40 当前时间:2020-07-06 19:06:27 任务1开始运行,时间为:2020-07-06 19:06:27 任务1结束运行,时间为:2020-07-06 19:06:27 任务2开始运行,时间为:2020-07-06 19:06:27 任务2开始运行,时间为:2020-07-06 19:06:28 ···

    上面实验可以看出,cancel试讲自身从任务队列中移除。

    1.4 Timer类的cancel()

    /** * 终止计时器,丢弃待调度中的任务,但不干扰正在进行中的任务。 */ public void cancel() { synchronized(queue) { thread.newTasksMayBeScheduled = false; queue.clear(); queue.notify(); // In case queue was already empty. } }

    进行实验:

    public class Test { private static Timer timer = new Timer(); public static class FirstTimerTask extends TimerTask { @Override public void run() { System.out.println("任务1开始运行,时间为:" + dateTimeFormat(new Date())); timer.cancel(); System.out.println("任务1结束运行,时间为:" + dateTimeFormat(new Date())); } } public static class SecondTimerTask extends TimerTask { @Override public void run() { System.out.println("任务2开始运行,时间为:" + dateTimeFormat(new Date())); } } public static void main(String[] args) throws ParseException { FirstTimerTask firstTimerTask = new FirstTimerTask(); Date task1StartTime = dateTimeFormat("2020-07-06 18:37:40"); System.out.println("任务1运行指定开始时间:" + dateTimeFormat(task1StartTime) + " 当前时间:" + dateTimeFormat(new Date())); SecondTimerTask secondTimerTask = new SecondTimerTask(); Date task2StartTime = dateTimeFormat("2020-07-06 18:37:40"); System.out.println("任务2运行指定开始时间:" + dateTimeFormat(task2StartTime) + " 当前时间:" + dateTimeFormat(new Date())); timer.schedule(firstTimerTask, task1StartTime, 1000); timer.schedule(secondTimerTask, task2StartTime, 1000); } private static String dateTimeFormat(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } private static Date dateTimeFormat(String date) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(date); } } 任务1运行指定开始时间:2020-07-06 18:37:40 当前时间:2020-07-06 19:10:49 任务2运行指定开始时间:2020-07-06 18:37:40 当前时间:2020-07-06 19:10:49 任务1开始运行,时间为:2020-07-06 19:10:49 任务1结束运行,时间为:2020-07-06 19:10:49 Process finished with exit code 0

    实验结果,任务被全部清除,并且进行被销毁。

    1.5 scheduleAtFixedRate(TimerTask task, Date firstTime, long period)

    schedule()和scheduleAtFixedRate()都会按照顺序执行。 schedule():如果执行任务的时间没有被延迟,那么下一次任务的执行时间参考的就是上一次任务的“开始”时间来计算,上面我们也做过对应实验。 scheduleAtFixedRate():如果执行任务的时间没有被延时,那么下一次执行时间参考的就是上一次任务的“结束时间来算”。 schedule()和scheduleAtFixedRate()执行任务的时间都被延时,那么下一次任务的执行时间参考的是上次任务“结束”的时间来计算。

    1.5.1 scheduleAtFixedRate执行任务不延时

    public class Test { private static Timer timer = new Timer(); private static int runCount = 0; public static class Task extends TimerTask { @Override public void run() { try { System.out.println("begin 运行时间为:" + dateTimeFormat(new Date())); Thread.sleep(2000); System.out.println(" end 运行时间为:" + dateTimeFormat(new Date())); } catch (InterruptedException e) { e.printStackTrace(); } runCount++; if (runCount == 3) { timer.cancel(); } } } public static void main(String[] args) throws ParseException { Task task = new Task(); String dateTime = "2020-07-06 19:31:00"; Date date = dateTimeFormat(dateTime); System.out.println("开始执行时间:" + dateTime + " 当前时间:" + dateTimeFormat(new Date())); timer.scheduleAtFixedRate(task, date, 10000); } private static String dateTimeFormat(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } private static Date dateTimeFormat(String date) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(date); } } begin 运行时间为:2020-07-06 19:31:00 end 运行时间为:2020-07-06 19:31:02 begin 运行时间为:2020-07-06 19:31:10 end 运行时间为:2020-07-06 19:31:12 begin 运行时间为:2020-07-06 19:31:20 end 运行时间为:2020-07-06 19:31:22

    结果表明:执行任务的时间没有被延时,则下次执行任务的时间是上次任务的开始时间+delay时间

    1.5.2 scheduleAtFixedRate执行任务延时

    public class Test { private static Timer timer = new Timer(); private static int runCount = 0; public static class Task extends TimerTask { @Override public void run() { try { System.out.println("begin 运行时间为:" + dateTimeFormat(new Date())); Thread.sleep(5000); System.out.println(" end 运行时间为:" + dateTimeFormat(new Date())); } catch (InterruptedException e) { e.printStackTrace(); } runCount++; if (runCount == 3) { timer.cancel(); } } } public static void main(String[] args) throws ParseException { Task task = new Task(); String dateTime = "2020-07-06 19:31:00"; Date date = dateTimeFormat(dateTime); System.out.println("开始执行时间:" + dateTime + " 当前时间:" + dateTimeFormat(new Date())); timer.scheduleAtFixedRate(task, date, 2000); } private static String dateTimeFormat(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } private static Date dateTimeFormat(String date) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(date); } } 开始执行时间:2020-07-06 19:31:00 当前时间:2020-07-06 19:33:40 begin 运行时间为:2020-07-06 19:33:40 end 运行时间为:2020-07-06 19:33:45 begin 运行时间为:2020-07-06 19:33:45 end 运行时间为:2020-07-06 19:33:50 begin 运行时间为:2020-07-06 19:33:50 end 运行时间为:2020-07-06 19:33:55

    1.5.3 schedule()和scheduleAtFixedRate()的追赶性验证

    schedule():

    public class Test { private static Timer timer = new Timer(); public static class Task extends TimerTask { @Override public void run() { System.out.println("运行时间为:" + dateTimeFormat(new Date())); } } public static void main(String[] args) throws ParseException { Task task = new Task(); String dateTime = "2020-07-06 19:39:00"; Date date = dateTimeFormat(dateTime); System.out.println("开始执行时间:" + dateTime + " 当前时间:" + dateTimeFormat(new Date())); timer.schedule(task, date, 5000); } private static String dateTimeFormat(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } private static Date dateTimeFormat(String date) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(date); } } 开始执行时间:2020-07-06 19:39:00 当前时间:2020-07-06 19:42:23 运行时间为:2020-07-06 19:42:23 运行时间为:2020-07-06 19:42:28 运行时间为:2020-07-06 19:42:33 运行时间为:2020-07-06 19:42:38 ···

    我们可以看到“2020-07-06 19:39:00”至“2020-07-06 19:42:23”之前的任务不会执行,这就是Task不追赶的情况。

    scheduleAtFixedRate():

    public class Test { private static Timer timer = new Timer(); public static class Task extends TimerTask { @Override public void run() { System.out.println("运行时间为:" + dateTimeFormat(new Date())); } } public static void main(String[] args) throws ParseException { Task task = new Task(); String dateTime = "2020-07-06 19:46:00"; Date date = dateTimeFormat(dateTime); System.out.println("开始执行时间:" + dateTime + " 当前时间:" + dateTimeFormat(new Date())); timer.scheduleAtFixedRate(task, date, 10000); } private static String dateTimeFormat(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } private static Date dateTimeFormat(String date) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(date); } } 开始执行时间:2020-07-06 19:46:00 当前时间:2020-07-06 19:47:00 运行时间为:2020-07-06 19:47:00 运行时间为:2020-07-06 19:47:00 运行时间为:2020-07-06 19:47:00 运行时间为:2020-07-06 19:47:00 运行时间为:2020-07-06 19:47:00 运行时间为:2020-07-06 19:47:00 运行时间为:2020-07-06 19:47:00 运行时间为:2020-07-06 19:47:10 ···

    可以看到时间段内的任务被执行了。

    2. Timer的弊端及替代品ScheduledExecutorService

    Timer是单线程的,所以task都是串行执行,如果上一个任务执行过久,那么将要执行的任务只能等上一个任务执行完成之后继续执行,性能较低。并且如果TimerTask.run()出现未捕获的异常时,那么该Timer的TimerThread将被中断。

    ScheduledExecutorService是多线程的,当给予线程池足够的线程数量,各任务都能够按照指定的频率执行,相比较Timer性能更高,满足的各场景要求也更多。如果任务执行过程中出现未捕获异常,那么ScheduledExecutorService也仅是终端该线程任务,其他任务正常执行。

    ScheduledExecutorService 继承了ExecutorService ,提供了四种调度方法,源码如下:

    public interface ScheduledExecutorService extends ExecutorService { /** * 创建延迟执行的单次任务 * * @param command 待执行任务 * @param delay 延迟时间 * @param unit 延时时间单位 * @return 任务完成 * @throws RejectedExecutionException 任务无法按计划执行时返回异常 * @throws NullPointerException 任务为空 */ public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit); /** * 创建延迟执行的单次任务 * * @param callable 待执行任务 * @param delay 延迟时间 * @param unit 延时时间单位 * @param <V> 任务执行完成返回数据类型ScheduledFuture<V> * @throws RejectedExecutionException 任务无法按计划执行时返回异常 * @throws NullPointerException 任务为空 */ public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit); /** * 等待initialDelay unit后首次执行task, 之后任务按照period unit的频率执行 * @param command 任务 * @param initialDelay 第一次延时执行时间 * @param period 任务间隔时间 * @param unit 时间单位 * @return a ScheduledFuture 任务完成 * @throws RejectedExecutionException 任务无法按计划执行时返回异常 * @throws NullPointerException 任务==null * @throws IllegalArgumentException period <=0 */ public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); /** * 等待initialDelay unit后首次执行task, 之后每个period unit 固定延时执行 * @param command 任务 * @param initialDelay 第一次延时执行时间 * @param period 任务间隔时间 * @param unit 时间单位 * @return a ScheduledFuture 任务完成 * @throws RejectedExecutionException 任务无法按计划执行时返回异常 * @throws NullPointerException null == command * @throws IllegalArgumentException delay <=0 */ public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); }

    scheduleAtFixedRate():固定频率是相对于任务执行的开始时间;scheduleWithFixedDelay():固定延迟是相对于任务执行的结束时间。 下面我们针对这四种任务调度方法写几个简单的示例。

    2.1 schedule Runnable

    创建延迟执行的单次任务

    public class Test { public static void main(String[] args) throws ExecutionException, InterruptedException { ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(); System.out.println("当前时间: " + dateTimeFormat(new Date())); try { service.schedule(new Runnable() { @Override public void run() { System.out.println("任务开始时间: " + dateTimeFormat(new Date())); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("任务结束时间: " + dateTimeFormat(new Date())); } }, 2, TimeUnit.SECONDS); System.out.println("main end " + dateTimeFormat(new Date())); } catch (Exception e) { e.printStackTrace(); } finally { service.shutdown(); if (!service.awaitTermination(5 * 1000, TimeUnit.MILLISECONDS)) { service.shutdownNow(); } } } private static String dateTimeFormat(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } } 当前时间: 2020-07-07 13:42:52 main end 2020-07-07 13:42:52 任务开始时间: 2020-07-07 13:42:54 任务结束时间: 2020-07-07 13:42:57 Process finished with exit code 0

    从打印信息来看,任务是异步执行的,由于参数delay = 2s,延迟两秒后任务开始。

    入参delay<= 0时,任务会立即执行。

    2.2 schedule Callable

    创建延迟执行的单次任务,同上面方法不同的入参一个是Runnable,此方法是Callable。 那么这边简单说一下Callable与Runnable的区别:

    Runnable没有返回值;Callable可以返回执行结果。 Runnable的run()方法异常只能内部捕获;Callable接口的call()方法允许向上抛出异常;

    public class Test { public static void main(String[] args) throws InterruptedException { ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(); System.out.println("当前时间: " + dateTimeFormat(new Date())); try { ScheduledFuture<String> future = service.schedule(new Callable<String>() { @Override public String call() { System.out.println("任务开始时间: " + dateTimeFormat(new Date())); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("任务结束时间: " + dateTimeFormat(new Date())); return "complete"; } }, 2, TimeUnit.SECONDS); System.out.println("main end " + dateTimeFormat(new Date())); System.out.println("任务执行完毕响应结果:" + future.get()); } catch (Exception e) { e.printStackTrace(); } finally { service.shutdown(); if (!service.awaitTermination(5 * 1000, TimeUnit.MILLISECONDS)) { service.shutdownNow(); } } } private static String dateTimeFormat(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } } 当前时间: 2020-07-07 13:48:25 main end 2020-07-07 13:48:25 任务开始时间: 2020-07-07 13:48:27 任务结束时间: 2020-07-07 13:48:30 任务执行完毕响应结果:complete Process finished with exit code 0

    打印结果如上所示,唯一的区别就是我们通过future.get()获取任务执行结果集,由于Future.get()方法是阻塞的,所以等到任务执行结束之后,响应结果才最终打印。

    2.3 scheduleAtFixedRate

    等待initialDelay unit后首次执行task, 之后任务按照period unit的频率执行

    public class Test { public static void main(String[] args) throws InterruptedException { ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(); System.out.println("当前时间: " + dateTimeFormat(new Date())); try { service.scheduleAtFixedRate(() -> { System.out.println("任务开始时间: " + dateTimeFormat(new Date())); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("任务结束时间: " + dateTimeFormat(new Date())); }, 10, 2, TimeUnit.SECONDS); System.out.println("main end " + dateTimeFormat(new Date())); } catch (Exception e) { e.printStackTrace(); } } private static String dateTimeFormat(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } } 当前时间: 2020-07-07 14:17:54 main end 2020-07-07 14:17:54 任务开始时间: 2020-07-07 14:18:04 任务结束时间: 2020-07-07 14:18:09 任务开始时间: 2020-07-07 14:18:09 任务结束时间: 2020-07-07 14:18:14 任务开始时间: 2020-07-07 14:18:14 ···

    观察上面的执行结果,延迟十秒开始执行任务,可能有同学有疑问,不是设置的执行间隔时间是2s吗?并且scheduleAtFixedRate为固定频率,为什么第二次执行任务的开始时间距离第一次的执行的开始时间间隔为5s呢?这是因为我们Thread.sleep(5000);,当一个任务执行时间过久,执行时间>=大于时间间隔,下次任务将立即执行。

    修改Thread.sleep(5000);为Thread.sleep(1000); 观察执行结果:

    当前时间: 2020-07-07 14:25:17 main end 2020-07-07 14:25:17 任务开始时间: 2020-07-07 14:25:27 任务结束时间: 2020-07-07 14:25:28 任务开始时间: 2020-07-07 14:25:29 任务结束时间: 2020-07-07 14:25:30 任务开始时间: 2020-07-07 14:25:31 任务结束时间: 2020-07-07 14:25:32 任务开始时间: 2020-07-07 14:25:33 任务结束时间: 2020-07-07 14:25:34 任务开始时间: 2020-07-07 14:25:35 ···

    scheduleAtFixedRate为固定频率,是相对于任务执行的开始时间period,查看响应信息打印,我们可以看到各任务开始时间距离上次任务开始时间间隔period unit。

    2.4 scheduleWithFixedDelay

    等待initialDelay unit后首次执行task, 之后每个period unit 固定延时执行

    下面同样的我们设置执行时间>period,代码如下:

    public class Test { public static void main(String[] args) { ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(); System.out.println("当前时间: " + dateTimeFormat(new Date())); try { service.scheduleWithFixedDelay(() -> { System.out.println("任务开始时间: " + dateTimeFormat(new Date())); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("任务结束时间: " + dateTimeFormat(new Date())); }, 10, 2, TimeUnit.SECONDS); System.out.println("main end " + dateTimeFormat(new Date())); } catch (Exception e) { e.printStackTrace(); } } private static String dateTimeFormat(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } } 当前时间: 2020-07-07 14:30:08 main end 2020-07-07 14:30:08 任务开始时间: 2020-07-07 14:30:18 任务结束时间: 2020-07-07 14:30:23 任务开始时间: 2020-07-07 14:30:25 任务结束时间: 2020-07-07 14:30:30 任务开始时间: 2020-07-07 14:30:32 任务结束时间: 2020-07-07 14:30:37 任务开始时间: 2020-07-07 14:30:39 ···

    结果可以看出来scheduleAtFixedRate是固定延迟的,固定延迟是相对于任务执行的结束时间。

    2.5 scheduleAtFixedRate与scheduleWithFixedDelay的实验结论

    scheduleAtFixedRate是固定延迟的,固定延迟是相对于任务执行的结束时间。 scheduleAtFixedRate为固定频率,固定频率是相对于任务执行的开始时间,当固定频率时间<任务执行时间,则下次任务开始时间等于上次任务执行结束时间。

    结语

    ScheduledThreadPoolExecutor的源码解读会放在后面,只能陆陆续续的写着,如有错误请指正,有兴趣的同学可以关注一下,不定期更新,感谢!

    Processed: 0.011, SQL: 9