在项目开发过程中,经常会遇到需要使用定时执行或延时执行任务的场景。比如我们在活动结束后自动汇总生成效果数据、导出Excel表并将文件通过邮件推送到用户手上,再比如微信运动每天都会在十点后向你发送个位数的步数(在?把摄像头从我家拆掉!)。
本文将会介绍java.util.Timer的使用,并从源码层面对它进行解析。
定时器Timer的使用
java.util.Timer是JDK提供的非常使用的工具类,用于计划在特定时间后执行的任务,可以只执行一次或定期重复执行。在JDK内部很多组件都是使用的java.util.Timer实现定时任务或延迟任务。
Timer可以创建多个对象的实例,每个对象都有且只有一个后台线程来执行任务。
Timer类是线程安全的,多个线程可以共享一个计时器,而无需使用任何的同步。
构造方法
首先我们可以看下Timer类的构造方法的API文档
- Timer(): 创建一个新的计时器。
- Timer(boolean isDaemon): 创建一个新的定时器,其关联的工作线程可以指定为守护线程。
- Timer(String name): 创建一个新的定时器,其关联的工作线程具有指定的名称。
- Timer(String name, boolean isDaemon): 创建一个新的定时器,其相关线程具有指定的名称,可以指定为守护线程。
Note: 守护线程是低优先级线程,在后台执行次要任务,比如垃圾回收。当有非守护线程在运行时,Java应用不会退出。如果所有的非守护线程都退出了,那么所有的守护线程也会随之退出。
实例方法
接下来我们看下Timer类的实例方法的API文档
- cancel(): 终止此计时器,并丢弃所有当前执行的任务。
- purge(): 从该计时器的任务队列中删除所有取消的任务。
- schedule(TimerTask task, Date time): 在指定的时间执行指定的任务。
- schedule(TimerTask task, Date firstTime, long period): 从指定 的时间开始 ,对指定的任务按照固定的延迟时间重复执行 。
- schedule(TimerTask task, long delay): 在指定的延迟之后执行指定的任务。
- schedule(TimerTask task, long delay, long period): 在指定的延迟之后开始 ,对指定的任务按照固定的延迟时间重复执行 。
- scheduleAtFixedRate(TimerTask task, Date firstTime, long period): 从指定的时间开始 ,对指定的任务按照固定速率重复执行 。
- scheduleAtFixedRate(TimerTask task, long delay, long period): 在指定的延迟之后开始 ,对指定的任务按照固定速率重复执行。
schedule和scheduleAtFixedRate都是重复执行任务,区别在于schedule是在任务成功执行后,再按照固定周期再重新执行任务,比如第一次任务从0s开始执行,执行5s,周期是10s,那么下一次执行时间是15s而不是10s。而scheduleAtFixedRate是从任务开始执行时,按照固定的时间再重新执行任务,比如第一次任务从0s开始执行,执行5s,周期是10s,那么下一次执行时间是10s而不是15s。
使用方式
1. 执行时间晚于当前时间
接下来我们将分别使用schedule(TimerTask task, Date time)和schedule(TimerTask task, long delay)用来在10秒后执行任务,并展示是否将Timer的工作线程设置成守护线程对Timer执行的影响。
首先我们创建类Task, 接下来我们的所有操作都会在这个类中执行, 在类中使用schedule(TimerTask task, Date time),代码如下
import java.util.Date; import java.util.Timer; import java.util.TimerTask; import static java.lang.System.currentTimeMillis; import static java.lang.Thread.currentThread; import static java.text.MessageFormat.format; public class Task { private static final long SECOND = 1000; public static void main(String[] args) { Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println(format("程序结束时间为: {0}", currentTimeMillis())); })); long startTimestamp = currentTimeMillis(); System.out.println(format("程序执行时间为: {0}", startTimestamp)); Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { long exceptedTimestamp = startTimestamp + 10 * SECOND; long executingTimestamp = currentTimeMillis(); long offset = executingTimestamp - exceptedTimestamp; System.out.println(format("任务运行在线程[{0}]上, 期望执行时间为[{1}], 实际执行时间为[{2}], 实际偏差[{3}]", currentThread().getName(), exceptedTimestamp, executingTimestamp, offset)); } }, new Date(startTimestamp + 10 * SECOND)); } }
在程序的最开始,我们注册程序结束时执行的函数,它用来打印程序的结束时间,我们稍后将会用它来展示工作线程设置为守护线程与非守护线程的差异。接下来是程序的主体部分,我们记录了程序的执行时间,定时任务执行时所在的线程、定时任务的期望执行时间与实际执行时间。
程序运行后的实际执行效果
程序执行时间为: 1,614,575,921,461 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,575,931,461], 实际执行时间为[1,614,575,931,464], 实际偏差[3]
程序在定时任务执行结束后并没有退出,我们注册的生命周期函数也没有执行,我们将在稍后解释这个现象。
接下来我们在类中使用schedule(TimerTask task, long delay), 来达到相同的在10秒钟之后执行的效果
import java.util.Timer; import java.util.TimerTask; import static java.lang.System.currentTimeMillis; import static java.lang.Thread.currentThread; import static java.text.MessageFormat.format; public class Task { private static final long SECOND = 1000; public static void main(String[] args) { Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println(format("程序结束时间为: {0}", currentTimeMillis())); })); Timer timer = new Timer(); long startTimestamp = currentTimeMillis(); System.out.println(format("程序执行时间为: {0}", startTimestamp)); timer.schedule(new TimerTask() { @Override public void run() { long exceptedTimestamp = startTimestamp + 10 * SECOND; long executingTimestamp = currentTimeMillis(); long offset = executingTimestamp - exceptedTimestamp; System.out.println(format("任务运行在线程[{0}]上, 期望执行时间为[{1}], 实际执行时间为[{2}], 实际偏差[{3}]", currentThread().getName(), exceptedTimestamp, executingTimestamp, offset)); } }, 10 * SECOND); } }
程序运行后的实际执行效果
程序执行时间为: 1,614,576,593,325 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,576,603,325], 实际执行时间为[1,614,576,603,343], 实际偏差[18]
回到我们刚刚的问题上,为什么我们的程序在执行完定时任务后没有正常退出?我们可以从Java API中对Thread类的描述中找到相关的内容:
从这段描述中,我们可以看到,只有在两种情况下,Java虚拟机才会退出执行
- 手动调用Runtime.exit()方法,并且安全管理器允许进行退出操作
- 所有的非守护线程都结束了,要么是执行完run()方法,要么是在run()方法中抛出向上传播的异常
所有的Timer在创建后都会创建关联的工作线程,这个关联的工作线程默认是非守护线程的,所以很明显我们满足第二个条件,所以程序会继续执行而不会退出。
那么如果我们将Timer的工作线程设置成守护线程会发生什么呢?
import java.util.Timer; import java.util.TimerTask; import static java.lang.System.currentTimeMillis; import static java.lang.Thread.currentThread; import static java.text.MessageFormat.format; public class Task { private static final long SECOND = 1000; public static void main(String[] args) { Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println(format("程序结束时间为: {0}", currentTimeMillis())); })); Timer timer = new Timer(true); long startTimestamp = currentTimeMillis(); System.out.println(format("程序执行时间为: {0}", startTimestamp)); timer.schedule(new TimerTask() { @Override public void run() { long exceptedTimestamp = startTimestamp + 10 * SECOND; long executingTimestamp = currentTimeMillis(); long offset = executingTimestamp - exceptedTimestamp; System.out.println(format("任务运行在线程[{0}]上, 期望执行时间为[{1}], 实际执行时间为[{2}], 实际偏差[{3}]", currentThread().getName(), exceptedTimestamp, executingTimestamp, offset)); } }, 10 * SECOND); } }
程序运行后的实际执行结果
程序执行时间为: 1,614,578,037,976 程序结束时间为: 1,614,578,037,996
可以看到我们的延迟任务还没有开始执行,程序就已经结束了,因为在我们的主线程退出后,所有的非守护线程都结束了,所以Java虚拟机会正常退出,而不会等待Timer中所有的任务执行完成后再退出。
2. 执行时间早于当前时间
如果我们是通过计算Date来指定执行时间的话,那么不可避免会出现一个问题——计算后的时间是早于当前时间的,这很常见,尤其是Java虚拟机会在不恰当的时候执行垃圾回收,并导致STW(Stop the world)。
接下来,我们将调整之前调用schedule(TimerTask task, Date time)的代码,让它在过去的时间执行
import java.util.Date; import java.util.Timer; import java.util.TimerTask; import static java.lang.System.currentTimeMillis; import static java.lang.Thread.currentThread; import static java.text.MessageFormat.format; public class Task { private static final long SECOND = 1000; public static void main(String[] args) { Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println(format("程序结束时间为: {0}", currentTimeMillis())); })); Timer timer = new Timer(); long startTimestamp = currentTimeMillis(); System.out.println(format("程序执行时间为: {0}", startTimestamp)); timer.schedule(new TimerTask() { @Override public void run() { long exceptedTimestamp = startTimestamp - 10 * SECOND; long executingTimestamp = currentTimeMillis(); long offset = executingTimestamp - exceptedTimestamp; System.out.println(format("任务运行在线程[{0}]上, 期望执行时间为[{1}], 实际执行时间为[{2}], 实际偏差[{3}]", currentThread().getName(), exceptedTimestamp, executingTimestamp, offset)); } }, new Date(startTimestamp - 10 * SECOND)); } }
程序运行后的执行结果
程序执行时间为: 1,614,590,000,184 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,589,990,184], 实际执行时间为[1,614,590,000,203], 实际偏差[10,019]
可以看到,当我们指定运行时间为过去时间时,Timer的工作线程会立执行该任务。
但是如果我们不是通过计算时间,而是期望延迟负数时间再执行,会发生什么呢?我们将调整之前调用schedule(TimerTask task, long delay)的代码, 让他以负数延迟时间执行
import java.util.Timer; import java.util.TimerTask; import static java.lang.System.currentTimeMillis; import static java.lang.Thread.currentThread; import static java.text.MessageFormat.format; public class Task { private static final long SECOND = 1000; public static void main(String[] args) { Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println(format("程序结束时间为: {0}", currentTimeMillis())); })); Timer timer = new Timer(); long startTimestamp = currentTimeMillis(); System.out.println(format("程序执行时间为: {0}", startTimestamp)); timer.schedule(new TimerTask() { @Override public void run() { long exceptedTimestamp = startTimestamp - 10 * SECOND; long executingTimestamp = currentTimeMillis(); long offset = executingTimestamp - exceptedTimestamp; System.out.println(format("任务运行在线程[{0}]上, 期望执行时间为[{1}], 实际执行时间为[{2}], 实际偏差[{3}]", currentThread().getName(), exceptedTimestamp, executingTimestamp, offset)); } }, -10 * SECOND); } }
程序运行后的执行结果
程序执行时间为: 1,614,590,267,556 Exception in thread "main" java.lang.IllegalArgumentException: Negative delay. at java.base/java.util.Timer.schedule(Timer.java:193) at cn.mgdream.schedule.Task.main(Task.java:22)
如果我们传入负数的延迟时间,那么Timer会抛出异常,告诉我们不能传入负数的延迟时间,这似乎是合理的——我们传入过去的时间是因为这是我们计算出来的,而不是我们主观传入的。在我们使用schedule(TimerTask task, long delay)需要注意这一点。
3. 向Timer中添加多个任务
接下来我们将分别向Timer中添加两个延迟任务,为了更容易地控制两个任务的调度顺序和时间,我们让第一个任务延迟5秒,第二个任务延迟10秒,同时让第一个任务阻塞10秒后再结束,通过这种方式来模拟出长任务。
import java.util.Timer; import java.util.TimerTask; import static java.lang.System.currentTimeMillis; import static java.lang.Thread.currentThread; import static java.text.MessageFormat.format; public class Task { private static final long SECOND = 1000; public static void main(String[] args) { Timer timer = new Timer(); long startTimestamp = currentTimeMillis(); System.out.println(format("程序执行时间为: {0}", startTimestamp)); timer.schedule(new TimerTask() { @Override public void run() { try { long exceptedTimestamp = startTimestamp + 5 * SECOND; long executingTimestamp = currentTimeMillis(); long offset = executingTimestamp - exceptedTimestamp; System.out.println(format("任务[0]运行在线程[{0}]上, 期望执行时间为[{1}], 实际执行时间为[{2}], 实际偏差[{3}]", currentThread().getName(), exceptedTimestamp, executingTimestamp, offset)); Thread.sleep(10 * SECOND); } catch (InterruptedException e) { e.printStackTrace(); } } }, 5 * SECOND); timer.schedule(new TimerTask() { @Override public void run() { long exceptedTimestamp = startTimestamp + 10 * SECOND; long executingTimestamp = currentTimeMillis(); long offset = executingTimestamp - exceptedTimestamp; System.out.println(format("任务[1]运行在线程[{0}]上, 期望执行时间为[{1}], 实际执行时间为[{2}], 实际偏差[{3}]", currentThread().getName(), exceptedTimestamp, executingTimestamp, offset)); } }, 10 * SECOND); } }
程序运行后的执行结果
程序执行时间为: 1,614,597,388,284 任务[0]运行在线程[Timer-0]上, 期望执行时间为[1,614,597,393,284], 实际执行时间为[1,614,597,393,308], 实际偏差[24] 任务[1]运行在线程[Timer-0]上, 期望执行时间为[1,614,597,398,284], 实际执行时间为[1,614,597,403,312], 实际偏差[5,028]
可以看到,两个任务在同个线程顺序执行,而第一个任务因为阻塞了10秒钟,所以是在程序开始运行后的第15秒结束,而第二个任务期望在第10秒结束,但是因为第一个任务还没有结束,所以第二个任务在第15秒开始执行,与与其执行时间偏差5秒钟。在使用Timer时尽可能不要执行长任务或使用阻塞方法,否则会影响后续任务执行时间的准确性。
4. 周期性执行任务
接下来我们将会分别使用schedule和scheduleAtFixedRate实现周期性执行任务。为了节省篇幅,我们将只演示如何使用schedule(TimerTask task, long delay, long period)和scheduleAtFixedRate(TimerTask task, long delay, long period)来实现周期性执行任务,并介绍它们的差异。而其他的两个方法schedule(TimerTask task, Date firstTime, long period)和scheduleAtFixedRate(TimerTask task, Date firstTime, long period)具有相同的效果和差异,就不再赘述。
首先我们修改Task类,调用schedule(TimerTask task, long delay, long period)来实现第一次执行完延迟任务后,周期性地执行任务
import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.atomic.AtomicLong; import static java.lang.System.currentTimeMillis; import static java.lang.Thread.currentThread; import static java.text.MessageFormat.format; public class Task { private static final long SECOND = 1000; public static void main(String[] args) { AtomicLong counter = new AtomicLong(0); Timer timer = new Timer(); long startTimestamp = currentTimeMillis(); System.out.println(format("程序执行时间为: {0}", startTimestamp)); timer.schedule(new TimerTask() { @Override public void run() { long count = counter.getAndIncrement(); long exceptedTimestamp = startTimestamp + 10 * SECOND + count * SECOND; long executingTimestamp = currentTimeMillis(); long offset = executingTimestamp - exceptedTimestamp; System.out.println(format("任务运行在线程[{0}]上, 期望执行时间为[{1}], 实际执行时间为[{2}], 实际偏差[{3}]", currentThread().getName(), exceptedTimestamp, executingTimestamp, offset)); } }, 10 * SECOND, SECOND); } }
修改后的代码和使用schedule(TimerTask task, long delay)时的代码基本相同,我们额外添加计数器来记录任务的执行次数,方法调用添加了第三个参数period,表示任务每次执行时到下一次开始执行的时间间隔,我们这里设置成1秒钟。
程序运行后的执行结果
程序执行时间为: 1,614,609,111,434 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,609,121,434], 实际执行时间为[1,614,609,121,456], 实际偏差[22] 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,609,122,434], 实际执行时间为[1,614,609,122,456], 实际偏差[22] 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,609,123,434], 实际执行时间为[1,614,609,123,457], 实际偏差[23] 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,609,124,434], 实际执行时间为[1,614,609,124,462], 实际偏差[28] 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,609,125,434], 实际执行时间为[1,614,609,125,467], 实际偏差[33] 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,609,126,434], 实际执行时间为[1,614,609,126,470], 实际偏差[36] 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,609,127,434], 实际执行时间为[1,614,609,127,473], 实际偏差[39] 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,609,128,434], 实际执行时间为[1,614,609,128,473], 实际偏差[39] 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,609,129,434], 实际执行时间为[1,614,609,129,474], 实际偏差[40]
可以看到,每次任务执行都会有一定时间的偏差,而这个偏差随着执行次数的增加而不断积累。这个时间偏差取决于Timer中需要执行的任务的个数,随着Timer中需要执行的任务的个数增加呈非递减趋势。因为这个程序现在只有一个任务在重复执行,因此每次执行的偏差不是很大,如果同时维护成百上千个任务,那么这个时间偏差会变得很明显。
接下来我们修改Task类,调用scheduleAtFixedRate(TimerTask task, long delay, long period)来实现周期性执行任务
import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.atomic.AtomicLong; import static java.lang.System.currentTimeMillis; import static java.lang.Thread.currentThread; import static java.text.MessageFormat.format; public class Task { private static final long SECOND = 1000; public static void main(String[] args) { AtomicLong counter = new AtomicLong(0); Timer timer = new Timer(); long startTimestamp = currentTimeMillis(); System.out.println(format("程序执行时间为: {0}", startTimestamp)); timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { long count = counter.getAndIncrement(); long exceptedTimestamp = startTimestamp + 10 * SECOND + count * SECOND; long executingTimestamp = currentTimeMillis(); long offset = executingTimestamp - exceptedTimestamp; System.out.println(format("任务运行在线程[{0}]上, 期望执行时间为[{1}], 实际执行时间为[{2}], 实际偏差[{3}]", currentThread().getName(), exceptedTimestamp, executingTimestamp, offset)); } }, 10 * SECOND, SECOND); } }
方法scheduleAtFixedRate(TimerTask task, long delay, long period)和schedule(TimerTask task, long delay)的效果基本相同,它们都可以达到周期性执行任务的效果,但是scheduleAtFixedRate方法会修正任务的下一次期望执行时间,按照每一次的期望执行时间加上period参数来计算出下一次期望执行时间,因此scheduleAtFixedRate是以固定速率重复执行的,而schedule则只保证两次执行的时间间隔相同。
程序运行后的执行结果
程序执行时间为: 1,614,610,372,927 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,610,383,927], 实际执行时间为[1,614,610,383,950], 实际偏差[23] 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,610,384,927], 实际执行时间为[1,614,610,384,951], 实际偏差[24] 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,610,385,927], 实际执行时间为[1,614,610,385,951], 实际偏差[24] 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,610,386,927], 实际执行时间为[1,614,610,386,947], 实际偏差[20] 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,610,387,927], 实际执行时间为[1,614,610,387,949], 实际偏差[22] 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,610,388,927], 实际执行时间为[1,614,610,388,946], 实际偏差[19] 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,610,389,927], 实际执行时间为[1,614,610,389,946], 实际偏差[19] 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,610,390,927], 实际执行时间为[1,614,610,390,947], 实际偏差[20] 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,610,391,927], 实际执行时间为[1,614,610,391,950], 实际偏差[23] 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,610,392,927], 实际执行时间为[1,614,610,392,946], 实际偏差[19]
5. 停止任务
尽管我们很少会主动停止任务,但是这里还是要介绍下任务停止的方式。
停止任务的方式分为两种:停止单个任务和停止整个Timer。
首先我们介绍如何停止单个任务,为了停止单个任务,我们需要调用TimerTask的cancal()方法,并调用Timer的purge()方法来移除所有已经被停止了的任务(回顾我们之前提到的,过多停止的任务不清空会影响我们的执行时间)
import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.atomic.AtomicLong; import static java.lang.System.currentTimeMillis; import static java.lang.Thread.currentThread; import static java.text.MessageFormat.format; public class Task { private static final long SECOND = 1000; public static void main(String[] args) { AtomicLong counter = new AtomicLong(0); Timer timer = new Timer(); long startTimestamp = currentTimeMillis(); System.out.println(format("程序执行时间为: {0}", startTimestamp)); TimerTask[] timerTasks = new TimerTask[4096]; for (int i = 0; i < timerTasks.length; i++) { final int serialNumber = i; timerTasks[i] = new TimerTask() { @Override public void run() { long count = counter.getAndIncrement(); long exceptedTimestamp = startTimestamp + 10 * SECOND + count * SECOND; long executingTimestamp = currentTimeMillis(); long offset = executingTimestamp - exceptedTimestamp; System.out.println(format("任务[{0}]运行在线程[{1}]上, 期望执行时间为[{2}], 实际执行时间为[{3}], 实际偏差[{4}]", serialNumber, currentThread().getName(), exceptedTimestamp, executingTimestamp, offset)); } }; } for (TimerTask timerTask : timerTasks) { timer.schedule(timerTask, 10 * SECOND, SECOND); } for (int i = 1; i < timerTasks.length; i++) { timerTasks[i].cancel(); } timer.purge(); } }
首先我们创建了4096个任务,并让Timer来调度它们,接下来我们把除了第0个任务外的其他4095个任务停止掉,并从Timer中移除所有已经停止的任务。
程序运行后的执行结果
程序执行时间为: 1,614,611,843,830 任务[0]运行在线程[Timer-0]上, 期望执行时间为[1,614,611,853,830], 实际执行时间为[1,614,611,853,869], 实际偏差[39] 任务[0]运行在线程[Timer-0]上, 期望执行时间为[1,614,611,854,830], 实际执行时间为[1,614,611,854,872], 实际偏差[42] 任务[0]运行在线程[Timer-0]上, 期望执行时间为[1,614,611,855,830], 实际执行时间为[1,614,611,855,875], 实际偏差[45] 任务[0]运行在线程[Timer-0]上, 期望执行时间为[1,614,611,856,830], 实际执行时间为[1,614,611,856,876], 实际偏差[46] 任务[0]运行在线程[Timer-0]上, 期望执行时间为[1,614,611,857,830], 实际执行时间为[1,614,611,857,882], 实际偏差[52] 任务[0]运行在线程[Timer-0]上, 期望执行时间为[1,614,611,858,830], 实际执行时间为[1,614,611,858,883], 实际偏差[53] 任务[0]运行在线程[Timer-0]上, 期望执行时间为[1,614,611,859,830], 实际执行时间为[1,614,611,859,887], 实际偏差[57] 任务[0]运行在线程[Timer-0]上, 期望执行时间为[1,614,611,860,830], 实际执行时间为[1,614,611,860,890], 实际偏差[60] 任务[0]运行在线程[Timer-0]上, 期望执行时间为[1,614,611,861,830], 实际执行时间为[1,614,611,861,891], 实际偏差[61] 任务[0]运行在线程[Timer-0]上, 期望执行时间为[1,614,611,862,830], 实际执行时间为[1,614,611,862,892], 实际偏差[62]
我们可以看到,只有第0个任务再继续执行,而其他4095个任务都没有执行。
接下来我们介绍如何使用Timer的cancel()来停止整个Timer的所有任务,其实很简单,只需要执行timer.cancel()就可以。
import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.atomic.AtomicLong; import static java.lang.System.currentTimeMillis; import static java.lang.Thread.currentThread; import static java.text.MessageFormat.format; public class Task { private static final long SECOND = 1000; public static void main(String[] args) { Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println(format("程序结束时间为: {0}", currentTimeMillis())); })); AtomicLong counter = new AtomicLong(0); Timer timer = new Timer(); long startTimestamp = currentTimeMillis(); System.out.println(format("程序执行时间为: {0}", startTimestamp)); TimerTask[] timerTasks = new TimerTask[4096]; for (int i = 0; i < timerTasks.length; i++) { final int serialNumber = i; timerTasks[i] = new TimerTask() { @Override public void run() { long count = counter.getAndIncrement(); long exceptedTimestamp = startTimestamp + 10 * SECOND + count * SECOND; long executingTimestamp = currentTimeMillis(); long offset = executingTimestamp - exceptedTimestamp; System.out.println(format("任务[{0}]运行在线程[{1}]上, 期望执行时间为[{2}], 实际执行时间为[{3}], 实际偏差[{4}]", serialNumber, currentThread().getName(), exceptedTimestamp, executingTimestamp, offset)); } }; } timer.cancel(); } }
在将所有的任务添加到Timer后,我们执行Timer对象的cancel()方法,为了更方便地表现出Timer的工作线程也终止了,我们注册了生命周期方法,来帮我们在程序结束后打印结束时间。
程序运行后的执行结果
程序执行时间为: 1,614,612,436,037 程序结束时间为: 1,614,612,436,061
可以看到,在执行Timer对象的cancel()方法后,Timer的工作线程也随之结束,程序正常退出。
源码解析
TimerTask
TimerTask类是一个抽象类,实现了Runnable接口
public abstract class TimerTask implements Runnable
TimerTask对象的成员
首先来看TimerTask类的成员部分
final Object lock = new Object(); int state = VIRGIN; static final int VIRGIN = 0; static final int SCHEDULED = 1; static final int EXECUTED = 2; static final int CANCELLED = 3; long nextExecutionTime; long period = 0;
对象lock是对外用来控制TimerTask对象修改的锁对象,它控制了锁的粒度——只会影响类属性的变更,而不会影响整个类的方法调用。接下来是state属性表示TimerTask对象的状态。nextExecutionTime属性表示TimerTask对象的下一次执行时间,当TimerTask对象被添加到任务队列后,将会使用这个属性来按照从小到大的顺序排序。period属性表示TimerTask对象的执行周期,period属性的值有三种情况
- 如果是0,那么表示任务不会重复执行
- 如果是正数,那么就表示任务按照相同的执行间隔来重复执行
- 如果是负数,那么就表示任务按照相同的执行速率来重复执行
TimerTask对象的构造方法
Timer对象的构造方法很简单,就是protected限定的默认构造方法,不再赘述
protected TimerTask() { }
TimerTask对象的成员方法
接下来我们看下TimerTask对象的成员方法
public abstract void run(); public boolean cancel() { synchronized(lock) { boolean result = (state == SCHEDULED); state = CANCELLED; return result; } } public long scheduledExecutionTime() { synchronized(lock) { return (period < 0 ? nextExecutionTime + period : nextExecutionTime - period); } }
首先是run()方法实现自Runnable()接口,为抽象方法,所有的任务都需要实现此方法。接下来是cancel()方法,这个方法会将任务的状态标记为CANCELLED,如果在结束前任务处于被调度状态,那么就返回true,否则返回false。至于scheduledExecutionTime()只是用来计算重复执行的下一次执行时间,在Timer中并没有被使用过,不再赘述。
TimerQueue
TimerQueue是Timer维护任务调度顺序的最小优先队列,使用的是最小二叉堆实现,如上文所述,排序用的Key是TimerTask的nextExecutionTime属性。
在介绍TimerQueue之前,我们先补充下数据结构的基础知识
二叉堆(Binary heap)
二叉堆是一颗除了最底层的元素外,所有层都被填满,最底层的元素从左向右填充的完全二叉树(complete binary tree)。完全二叉树可以用数组表示,假设元素从1开始编号,下标为i的元素,它的左孩子的下标为2*i,它的右孩子的下标为2*i+1。
二叉堆的任意非叶节点满足堆序性:假设我们定义的是最小优先队列,那么我们使用的是小根堆,任意节点的元素值都小于它的左孩子和右孩子(如果有的话)的元素值。
二叉堆的定义满足递归定义法,即二叉堆的任意子树都是二叉堆,单个节点本身就是二叉堆。
根据堆序性和递归定义法,二叉堆的根节点一定是整个二叉堆中元素值最小的节点。
与堆结构有关的操作,除了add, getMin和removeMin之外,还有fixUp、fixDown和heapify三个关键操作,而add、getMin和removeMin也是通过这三个操作来完成的,下面来简单介绍下这三个操作
- fixUp: 当我们向二叉堆中添加元素时,我们可以简单地将它添加到二叉树的末尾,此时从这个节点到根的完整路径上不满足堆序性。之后将它不断向上浮,直到遇到比它小的元素,此时整个二叉树的所有节点都满足堆序性。当我们减少了二叉堆中元素的值的时候也可以通过这个方法来维护二叉堆。
- fixDown: 当我们从二叉堆中删除元素时,我们可以简单地将二叉树末尾的元素移动到根,此时不一定满足堆序性,之后将它不断下沉,直到遇到比它大的元素,此时整个二叉树的所有节点都满足堆序性。当我们增加了二叉堆中元素的值的时候也可以通过这个方法来维护二叉堆。
- heapify: 当我们拿到无序的数组的时候,也可以假设我们拿到了一棵不满足堆序性的二叉树,此时我们将所有的非叶节点向下沉,直到整个二叉树的所有节点都满足堆序性,此时我们得到了完整的二叉堆。这个操作是原地操作,不需要额外的空间复杂度,而时间复杂度是O(N)。
关于二叉堆的详细内容将会在后续的文章中展开详解,这里只做简单的介绍,了解这些我们就可以开始看TimerQueue的源码。
TimerQueue的完整代码
我们直接来看TaskQueue的完整代码
class TaskQueue { private TimerTask[] queue = new TimerTask[128]; private int size = 0; int size() { return size; } void add(TimerTask task) { // Grow backing store if necessary if (size + 1 == queue.length) queue = Arrays.copyOf(queue, 2*queue.length); queue[++size] = task; fixUp(size); } TimerTask getMin() { return queue[1]; } TimerTask get(int i) { return queue[i]; } void removeMin() { queue[1] = queue[size]; queue[size--] = null; // Drop extra reference to prevent memory leak fixDown(1); } void quickRemove(int i) { assert i <= size; queue[i] = queue[size]; queue[size--] = null; // Drop extra ref to prevent memory leak } void rescheduleMin(long newTime) { queue[1].nextExecutionTime = newTime; fixDown(1); } boolean isEmpty() { return size==0; } void clear() { // Null out task references to prevent memory leak for (int i=1; i 1) { int j = k >> 1; if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime) break; TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp; k = j; } } private void fixDown(int k) { int j; while ((j = k << 1) 0) { if (j < size && queue[j].nextExecutionTime > queue[j+1].nextExecutionTime) j++; // j indexes smallest kid if (queue[k].nextExecutionTime = 1; i--) fixDown(i); } }
按照我们之前介绍的二叉堆的相关知识,我们可以看到TimerQueue维护了TimerTask的数组queue,初始大小size为0。
add操作首先判断了数组是否满了,如果数组已经满了,那么先执行扩容操作,再进行添加操作。如上所述,add操作先将元素放到二叉树末尾的元素(queue[++size]),之后对这个元素进行上浮来维护堆序性。
getMin直接返回二叉树的树根(queue[1]),get方法直接返回数组的第i个元素。removeMin方法会将二叉树末尾的元素(queue[size])移动到树根(queue[1]),并将原本二叉树末尾的元素设置成null,来让垃圾回收器回收这个TimerTask,之后执行fixDown来维护堆序性,quickRemove也是相同的过程,只不过它在移动元素后没有执行下沉操作,当连续执行多次quickRemove后统一执行heapify来维护堆序性。
rescheduleMin会将树根元素的元素值设置成newTime,并将它下沉到合适的位置。
fixUp、fixDown和heapify操作就如上文所述,用来维护二叉堆的读序性。不过这里面实现的fixUp和fixDown并不优雅,基于交换临位元素的实现需要使用T(3log(N))的时间,而实际上有T(log(N))的实现方法。后续的文章中会详细介绍优先队列与二叉堆的实现方式。
TimerThread
我们直接来看TimerThread的代码
class TimerThread extends Thread { boolean newTasksMayBeScheduled = true; private TaskQueue queue; TimerThread(TaskQueue queue) { this.queue = queue; } public void run() { try { mainLoop(); } finally { // Someone killed this Thread, behave as if Timer cancelled synchronized(queue) { newTasksMayBeScheduled = false; queue.clear(); // Eliminate obsolete references } } } private void mainLoop() { while (true) { try { TimerTask task; boolean taskFired; synchronized(queue) { // Wait for queue to become non-empty while (queue.isEmpty() && newTasksMayBeScheduled) queue.wait(); if (queue.isEmpty()) break; // Queue is empty and will forever remain; die // Queue nonempty; look at first evt and do the right thing long currentTime, executionTime; task = queue.getMin(); synchronized(task.lock) { if (task.state == TimerTask.CANCELLED) { queue.removeMin(); continue; // No action required, poll queue again } currentTime = System.currentTimeMillis(); executionTime = task.nextExecutionTime; if (taskFired = (executionTime<=currentTime)) { if (task.period == 0) { // Non-repeating, remove queue.removeMin(); task.state = TimerTask.EXECUTED; } else { // Repeating task, reschedule queue.rescheduleMin( task.period<0 ? currentTime - task.period : executionTime + task.period); } } } if (!taskFired) // Task hasn't yet fired; wait queue.wait(executionTime - currentTime); } if (taskFired) // Task fired; run it, holding no locks task.run(); } catch(InterruptedException e) { } } } }
首先是控制变量newTasksMayBeScheduled,表示当前工作线程是否应该继续执行任务,当它为false的时候它将不会再从任务队列中取任务执行,表示当前工作线程已结束。接下来的queue变量是通过构造方法传进来的任务队列,工作线程的任务队列与Timer共享,实现生产消费者模型。
进入到run()方法,run()方法会调用mainLoop()方法来执行主循环,而finally代码块会在主循环结束后清空任务队列实现优雅退出。
在mainLoop()方法中执行了死循环来拉取执行任务,在死循环中首先获取queue的锁来实现线程同步,接下来判断任务队列是否为且工作线程是否停止,如果任务队列为空且工作线程未停止,那么就使用queue.wait()来等待Timer添加任务后唤醒该线程,Object#wait()方法会释放当前线程所持有的该对象的锁,关于wait/notisfy的内容可以去看Java API相关介绍。如果queue退出等待后依旧为空,则表示newTasksMayBeScheduled为false,工作线程已停止,退出主循环,否则会从任务队列中取出需要最近执行的任务(并不会删除任务)。
取到需要最近执行的任务后,获取该任务的锁,并判断该任务是否已经停止,如果该任务已经停止,那么就把它从任务队列中移除,并什么都不做继续执行主循环。接下来判断当前时间是否小于等于任务的下一次执行时间,如果满足条件则将taskFired设置成true,判断当前任务是否需要重复执行。如果不需要重复执行就将它从任务队列中移除,并将任务状态设置成EXECUTED,如果需要重复执行就根据period设置它的下一次执行时间并重新调整任务队列。
完成这些操作后,如果taskFired为false,就让queue对象进入有限等待状态,很容易得到我们需要的最大等待时间为executionTime - currentTime。如果taskFired为true,那么就释放锁并执行被取出的任务。
Timer
Timer对象的成员
首先来看Timer的成员部分
private final TaskQueue queue = new TaskQueue(); private final TimerThread thread = new TimerThread(queue); private final Object threadReaper = new Object() { @SuppressWarnings("deprecation") protected void finalize() throws Throwable { synchronized(queue) { thread.newTasksMayBeScheduled = false; queue.notify(); // In case queue is empty. } } }; private static final AtomicInteger nextSerialNumber = new AtomicInteger(0);
其中queue对象是如前面所说,为了任务调度的最小优先队列。接下来是TimerThread,它是Timer的工作线程,在Timer创建时就已经被分配,并与Timer共享任务队列。
threadReaper是一个只复写了finalize方法的对象,它的作用是当Timer对象没有存活的引用后,终止任务线程,并等待任务队列中的所有任务执行结束后退出工作线程,实现优雅退出。
nextSerialNumber用来记录工作线程的序列号,全局唯一,避免生成的线程名称冲突。
Timer对象的构造方法
接下来我们看下Timer的所有构造方法
public Timer() { this("Timer-" + serialNumber()); } public Timer(boolean isDaemon) { this("Timer-" + serialNumber(), isDaemon); } public Timer(String name) { thread.setName(name); thread.start(); } public Timer(String name, boolean isDaemon) { thread.setName(name); thread.setDaemon(isDaemon); thread.start(); }
可以看到,所有的构造构造方法所做的事都相同:设置工作线程属性,并启动工作线程。
成员函数
接下来我们可以看下Timer的成员函数,我们首先不考虑cancel()和purge()方法,直接看schedule系列方法
public void schedule(TimerTask task, long delay) { if (delay < 0) throw new IllegalArgumentException("Negative delay."); sched(task, System.currentTimeMillis()+delay, 0); } public void schedule(TimerTask task, Date time) { sched(task, time.getTime(), 0); } 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); } public void schedule(TimerTask task, Date firstTime, long period) { if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, firstTime.getTime(), -period); } 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); } public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) { if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, firstTime.getTime(), period); }
可以看到,所有的schedule方法除了做参数教研外,都将延迟时间和计划执行时间转化为时间戳委托给sched方法来执行。schedule和scheduleAtFixedRate传递的参数都相同,不过在传递period参数时使用符号来区分周期执行的方式。
接下来我们可以看下这位神秘嘉宾——sched方法到底做了哪些事
private void sched(TimerTask task, long time, long period) { if (time < 0) throw new IllegalArgumentException("Illegal execution time."); // Constrain value of period sufficiently to prevent numeric // overflow while still being effectively infinitely large. 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(); } }
sched方法首先做了一些参数校验,保证期待执行时间不小于0,且执行周期不至于太大。接下来获取任务队列queue对象的monitor(监视器锁),如果Timer的工作线程已经被停止了,那么就会抛出IllegalStateException来禁止继续添加任务,newTasksMayBeScheduled这个变量将会在稍后介绍。之后sched方法会尝试获取task.lock对象的锁,判断task的状态避免重复添加,并设置task的下一次执行时间、task的执行周期和状态。之后将task添加到任务队列中,如果当前任务就是执行时间最近的任务,那么就会唤起等待queue对象的线程(其实就是thread工作线程)继续执行。
总结
本文从各个方面介绍了java.util.Timer类的使用方式,并从源码角度介绍了java.util.Timer的实现方式。看完本文后,读者应当掌握
- 如何执行晚于当前时间的任务
- 当任务执行时间早于当前时间会发生什么
- 如何向Timer中添加多个任务
- 如何周期性执行任务
- 如何停止任务
- 如何自己实现类似的定时器
希望本文可以帮助大家在工作中更好地使用java.util.Timer。