您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
深入探究JDK中Timer的使用方式
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
深入探究JDK中Timer的使用方式
自猿其说Tech
2021-03-25
IP归属:未知
376360浏览
计算机编程
### 导言 在项目开发过程中,经常会遇到需要使用定时执行或延时执行任务的场景。比如我们在活动结束后自动汇总生成效果数据、导出Excel表并将文件通过邮件推送到用户手上,再比如微信运动每天都会在十点后向你个位数的步数(在?把摄像头从我家拆掉!)。 本文将会介绍java.util.Timer的使用,并从源码层面对它进行解析。 #### 定时器Timer的使用 java.util.Timer是JDK提供的非常使用的工具类,用于计划在特定时间后执行的任务,可以只执行一次或定期重复执行。在JDK内部很多组件都是使用的java.util.Timer实现定时任务或延迟任务。 Timer可以创建多个对象的实例,每个对象都有且只有一个后台线程来执行任务。 ![](https://oscimg.oschina.net/oscnet/up-04c5993c2d22aebcce803c0bdb0e671931c.png) Timer类是线程安全的,多个线程可以共享一个计时器,而无需使用任何的同步。 #### 构造方法 首先我们可以看下Timer类的构造方法的API文档 ![](https://oscimg.oschina.net/oscnet/up-472c6e68da48afeb29efd01a97c22a5176a.png) **Timer()**: 创建一个新的计时器。 **Timer(boolean isDaemon)**: 创建一个新的定时器,其关联的工作线程可以指定为守护线程。 **Timer(String name)**: 创建一个新的定时器,其关联的工作线程具有指定的名称。 **Timer(String name, boolean isDaemon)**: 创建一个新的定时器,其相关线程具有指定的名称,可以指定为守护线程。 **Note**: 守护线程是低优先级线程,在后台执行次要任务,比如垃圾回收。当有非守护线程在运行时,Java应用不会退出。如果所有的非守护线程都退出了,那么所有的守护线程也会随之退出。 #### 实例方法 接下来我们看下Timer类的实例方法的API文档 ![](https://oscimg.oschina.net/oscnet/up-5ab53ee385ec8555da8c70659c9ebeb8e77.png) **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)`,代码如下 ```java 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)); } } ``` 在程序的最开始,我们注册程序结束时执行的函数,它用来打印程序的结束时间,我们稍后将会用它来展示工作线程设置为守护线程与非守护线程的差异。接下来是程序的主体部分,我们记录了程序的执行时间,定时任务执行时所在的线程、定时任务的期望执行时间与实际执行时间。 ** 程序运行后的实际执行效果** ```javascript 程序执行时间为: 1,614,575,921,461 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,575,931,461], 实际执行时间为[1,614,575,931,464], 实际偏差[3] ``` 程序在定时任务执行结束后并没有退出,我们注册的生命周期函数也没有执行,我们将在稍后解释这个现象。 接下来我们在类中使用`schedule(TimerTask task, long delay)`, 来达到相同的在10秒钟之后执行的效果 ```java 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); } } ``` **程序运行后的实际执行效果** ```javascript 程序执行时间为: 1,614,576,593,325 任务运行在线程[Timer-0]上, 期望执行时间为[1,614,576,603,325], 实际执行时间为[1,614,576,603,343], 实际偏差[18] ``` 回到我们刚刚的问题上,为什么我们的程序在执行完定时任务后没有正常退出?我们可以从Java API中对Thread类的描述中找到相关的内容: ![](https://oscimg.oschina.net/oscnet/up-f2787701f8f51b19e7d60dc6e13d09eedd0.png) 从这段描述中,我们可以看到,只有在两种情况下,Java虚拟机才会退出执行 手动调用Runtime.exit()方法,并且安全管理器允许进行退出操作 所有的非守护线程都结束了,要么是执行完run()方法,要么是在run()方法中抛出向上传播的异常 所有的Timer在创建后都会创建关联的工作线程,这个关联的工作线程默认是非守护线程的,所以很明显我们满足第二个条件,所以程序会继续执行而不会退出。 那么如果我们将`Timer`的工作线程设置成守护线程会发生什么呢? ```java 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); } } ``` **程序运行后的实际执行结果** ```javascript 程序执行时间为: 1,614,578,037,976 程序结束时间为: 1,614,578,037,996 ``` 可以看到我们的延迟任务还没有开始执行,程序就已经结束了,因为在我们的主线程退出后,所有的非守护线程都结束了,所以Java虚拟机会正常退出,而不会等待`Timer`中所有的任务执行完成后再退出。 **2. 执行时间早于当前时间** 如果我们是通过计算`Date`来指定执行时间的话,那么不可避免会出现一个问题——计算后的时间是早于当前时间的,这很常见,尤其是Java虚拟机会在不恰当的时候执行垃圾回收,并导致STW(Stop the world)。 接下来,我们将调整之前调用`schedule(TimerTask task, Date time)`的代码,让它在过去的时间执行 ```java 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)); } } ``` **程序运行后的执行结果** ```javascript 程序执行时间为: 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)`的代码, 让他以负数延迟时间执行 ```java 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); } } ``` **程序运行后的执行结果** ```javascript 程序执行时间为: 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秒后再结束,通过这种方式来模拟出长任务。 ```java 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); } } ``` **程序运行后的执行结果** ```javascript 程序执行时间为: 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)`来实现第一次执行完延迟任务后,周期性地执行任务 ```java 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秒钟。 **程序运行后的执行结果** ```javascript 程序执行时间为: 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)`来实现周期性执行任务 ```java 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则只保证两次执行的时间间隔相同。 **程序运行后的执行结果** ```javascript 程序执行时间为: 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()`方法来移除所有已经被停止了的任务(回顾我们之前提到的,过多停止的任务不清空会影响我们的执行时间) ```java 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`中移除所有已经停止的任务。 **程序运行后的执行结果** ```java 程序执行时间为: 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()`就可以。 ```java 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`的工作线程也终止了,我们注册了生命周期方法,来帮我们在程序结束后打印结束时间。 程序运行后的执行结果 ```java 程序执行时间为: 1,614,612,436,037 程序结束时间为: 1,614,612,436,061 ``` 可以看到,在执行`Timer`对象的`cancel()`方法后,`Timer`的工作线程也随之结束,程序正常退出。 #### 总结 本文从介绍了`java.util.Timer`的使用方式,覆盖了我们日常使用中涉及到的绝大部分场景和可能会遇到的问题。在接下来的文章中还会从源码角度对`java.util.Timer`进行解析,敬请期待~ ------------ ###### 自猿其说Tech-JDL京东物流技术发展部 ###### 作者:中台技术部 纪卓志
原创文章,需联系作者,授权转载
上一篇:Widget开发实践
下一篇:DBeaver免费开源的数据库客户端工具
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
最新回复
丨
点赞排行
共0条评论
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号