在项目开发过程中,经常会遇到需要使用定时执行或延时执行任务的场景。比如我们在活动结束后自动汇总生成效果数据、导出Excel表并将文件通过邮件推送到用户手上,再比如微信运动每天都会在十点后向你推送个位数的微信步数。
本文将从各个场景介绍java.util.Timer
的使用方式以及可能会出现的问题。
java.util.Timer
是JDK提供的非常实用的工具类,用于计划在特定时间后执行的任务,可以只执行一次或定期重复执行。在JDK内部很多组件都是使用的java.util.Timer
实现定时任务或延迟任务。
Timer
可以创建多个对象的实例,每个对象都有且只有一个后台线程来执行任务。
Timer类是线程安全的,多个线程可以共享一个计时器,而无需使用任何的同步。
首先我们可以看下Timer类的构造方法的API文档
1.Timer(): 创建一个新的计时器。
2.Timer(boolean isDaemon): 创建一个新的定时器,其关联的工作线程可以指定为守护线程。
3.Timer(String name): 创建一个新的定时器,其关联的工作线程具有指定的名称。
4.Timer(String name, boolean isDaemon): 创建一个新的定时器,其相关线程具有指定的名称,可以指定为守护线程。
Note: 守护线程是低优先级线程,在后台执行次要任务,比如垃圾回收。当有非守护线程在运行时,Java应用不会退出。如果所有的非守护线程都退出了,那么所有的守护线程也会随之退出。
接下来我们看下Timer类的实例方法的API文档
1.cancel(): 终止此计时器,并丢弃所有当前执行的任务。
2.purge(): 从该计时器的任务队列中删除所有取消的任务。
3.schedule(TimerTask task, Date time): 在指定的时间执行指定的任务。
4.schedule(TimerTask task, Date firstTime, long period): 从指定 的时间开始 ,对指定的任务按照固定的延迟时间重复执行 。
5.schedule(TimerTask task, long delay): 在指定的延迟之后执行指定的任务。
6.schedule(TimerTask task, long delay, long period): 在指定的延迟之后开始 ,对指定的任务按照固定的延迟时间重复执行 。
7.scheduleAtFixedRate(TimerTask task, Date firstTime, long period): 从指定的时间开始 ,对指定的任务按照固定速率重复执行 。
8.scheduleAtFixedRate(TimerTask task, long delay, long period): 在指定的延迟之后开始 ,对指定的任务按照固定速率重复执行。
schedule
和scheduleAtFixedRate
都是重复执行任务,区别在于schedule
是在任务成功执行后,再按照固定周期再重新执行任务,比如第一次任务从0s开始执行,执行5s,周期是10s,那么下一次执行时间是15s而不是10s。而scheduleAtFixedRate
是从任务开始执行时,按照固定的时间再重新执行任务,比如第一次任务从0s开始执行,执行5s,周期是10s,那么下一次执行时间是10s而不是15s。
接下来我们将分别使用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虚拟机才会退出执行
1.手动调用Runtime.exit()
方法,并且安全管理器允许进行退出操作
2.所有的非守护线程都结束了,要么是执行完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
中所有的任务执行完成后再退出。
如果我们是通过计算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)
需要注意这一点。
接下来我们将分别向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时尽可能不要执行长任务或使用阻塞方法,否则会影响后续任务执行时间的准确性。
接下来我们将会分别使用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]
尽管我们很少会主动停止任务,但是这里还是要介绍下任务停止的方式。停止任务的方式分为两种:停止单个任务和停止整个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
对象的c
ancel()方法后,Timer
的工作线程也随之结束,程序正常退出。
本文从介绍了java.util.Timer的使用方式,覆盖了我们日常使用中涉及到的绝大部分场景和可能会遇到的问题。在接下来的文章中还会从源码角度对java.util.Timer进行解析,敬请期待~