导言
在项目开发过程中,经常会遇到需要使用定时执行或延时执行任务的场景。比如我们在活动结束后自动汇总生成效果数据、导出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()
方法,并且安全管理器允许进行退出操作 - 所有的非守护线