导言
在项目开发过程中,经常会遇到需要使用定时执行或延时执行任务的场景。比如我们在活动结束后自动汇总生成效果数据、导出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): 创建一个新的定时器,其相关线程具有指定的名称,可以指定为守护线程。
实例方法
接下来我们看下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): 在指定的延迟之后开始 ,对指定的任务按照固定速率重复执行。
使用方式
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<=size; i++)
queue[i] = null;
size = 0;
}
private void fixUp(int k) {
while (k > 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) <= size && j > 0) {
if (j < size &&
queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)
j++; // j indexes smallest kid
if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)
break;
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
k = j;
}
}
void heapify() {
for (int i = size/2; i >= 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
。