java.util.Timer 是 Java 中的一个有用类,它能够用来组织在未来某个时刻履行的使命,或许守时履行使命。它内部包含一个使命行列,用于存储要履行的 TimerTask。经过 schedule 或 scheduleAtFixedRate 办法组织使命。
替代方案
Java 5.0 引入了 J.U.C. ,其间一个并发工具是 ScheduledThreadPoolExecutor ,它是一个线程池,用于以给定的速率或推迟重复履行使命。它实践上是 Timer/TimerTask 组合的更通用的替代品,由于它允许多个服务线程,承受各种时刻单位,而且不需求承继 TimerTask (只需完结 Runnable )。
运用一个线程配置 ScheduledThreadPoolExecutor 作用等同于 Timer 。
注意事项
1. 最好不要在 TimerTask 中做耗时操作
Timer 内部会相关一个单独的线程,而且自己会有一个依照下次履行时刻排序的使命行列,线程会按序履行行列中的使命。每个使命不应该产生耗时操作,由于假如某一个使命花费了很多时刻运转,会占用当时后台线程资源,导致后续的使命产生推迟。这些使命或许堆到最终,并在产生推迟的使命履行完结后,快速接连履行。
2. 最好主动调用 cancel 开释 Timer 的线程资源
当 Timer 目标履行完一切的使命后,而且没有了实时引证,Timer 相关的线程才会停止(并成为废物搜集的目标)。可是,这或许需求很长的时刻才能产生。默许情况下,使命履行线程不作为看护进程线程运转,因而它能够防止应用程序停止。假如调用者想要快速停止计时器的使命履行线程,调用者应该调用计时器的 cancel 办法。
3. TimerThread 意外停止,会抛出 IllegalStateException
假如 Timer 的 TimerThread 意外停止,例如,由于调用了它的 stop 办法,任何进一步测验调度计时器上的使命都将导致 IllegalStateException,就像调用了计时器的 cancel 办法相同。
4. 线程安全及实时确保
Timer 是线程安全的,多个线程能够同享一个 Timer 目标,而不需求外部同步。Timer 并不供给实时确保,由于它是经过 Object.wait(long)
调度使命的。
5. 并发及时刻复杂度
这个类能够扩展到很多的并发方案使命(数千个应该没有问题)。在内部,它运用 **二进制堆 **来表明它的使命行列,因而调度使命的时刻复杂度是 O(log n),其间 n 是并发调度使命的数量。
运用办法
private fun startTimer() {
var timeTask = object : TimerTask() {
override fun run() {
// do somethings
}
}
timer = Timer()
// 开端履行
timer.schedule(task, DELAY_TIME, PERIOD_TIME)
// 撤销履行
timer.cancel()
}
运用 Timer 有四个过程:
- 结构一个 Timer 履行的使命
- 创立一个 Timer 目标
- 调用 Timer#schedule 系列办法,设置好履行的使命、推迟时刻和重复履行的周期间隔时刻
- 在恰当的时机调用 cancel 办法,撤销守时器。
运转流程
Timer 能够按方案履行重复的使命或许守时履行指定使命,这是由于 Timer 内部利用了一个后台线程 TimerThread 有方案地履行指定使命。Timer 内部有一个使命行列,它能够依照时刻先后顺序履行使命,而 TimerTask 便是一个使命。TimerTask 是一个抽象类,它的子类代表一个能够被 Timer 方案的使命。TimerTask 中有一个 run 办法,当 Timer 方案的时刻抵达时,Timer 就会履行 TimerTask 的 run 办法。TimerTask 的履行是在 Timer 的内部线程中履行的,因而 TimerTask 不应该履行一个耗时的操作,不然会影响 Timer 的精度。TimerTask 的履行时刻不能超过 Timer 的周期,不然 Timer 会抛出异常。Timer 内部有一个 TimerQueue,它是一个优先级行列,用于寄存 TimerTask。TimerQueue 中的 TimerTask 依照履行时刻排序,履行时刻早的 TimerTask 优先级高。
他们的联系如下:
- Timer 类负责组织使命的履行。它包含一个使命行列,该行列保存要履行的 TimerTask。
- TimerTask 是一个抽象类,它完结了 Runnable 接口。您需求创立一个承继自 TimerTask 的类,并重写 run 办法来界说要履行的使命。
- 当您运用 Timer 的 schedule 或 scheduleAtFixedRate 办法组织使命时,这些使命将被增加到 TimerQueue 中。
- TimerThread 是一个内部类,它承继自 Thread 类。它负责从行列中获取使命并履行它们。
TimerTask
public abstract class TimerTask implements Runnable {
// 守时器使命要履行的动作
public abstract void run();
}
TimerTask 是一个抽象类,它完结了 Runnable 接口,运用者需求创立一个承继自 TimerTask 的类,并重写 run 办法来界说要履行的使命。
TimerTask 的状况
TimerTask 内部界说了当时使命的状况:
public abstract class TimerTask implements Runnable {
// 此使命的状况,从下面的常量中挑选
int state = VIRGIN;
// 这个使命还没有被组织
static final int VIRGIN = 0;
// 方案履行该使命。假如它是非重复使命,则表明没有履行
static final int SCHEDULED = 1;
// 此非重复使命已履行(或当时正在履行),且未被撤销。
static final int EXECUTED = 2;
// 这个使命现已被撤销(调用TimerTask.cancel)
static final int CANCELLED = 3;
// other codes
}
TimerTask 经过 state 保存当时使命的履行状况,后续会依据这个特点来进行状况判断。
关于时刻操控,有两个成员特点:
// 假定该使命方案履行,此使命的下一次履行时刻,由 System.currentTimeMillis 回来,
// 关于重复使命,此字段将在每个使命履行之前更新。
long nextExecutionTime;
// 周期(以毫秒为单位)用于重复使命。正值表明固定速率履行。负值表明固定推迟履行。
// 0表明非重复使命。
long period = 0;
核算下次履行时刻
public long scheduledExecutionTime() {
synchronized(lock) {
return (period < 0 ? nextExecutionTime + period
: nextExecutionTime - period);
}
}
使命的履行时刻是依据下一次履行时刻 + 使命周期 来进行的,假如 period 小于 0,表明这个固定推迟的使命,尽管无法解释固定推迟的含义,可是这个办法通常不与固定推迟履行重复使命一起运用,由于它们的方案履行时刻允许随时刻漂移,因而不是特别重要。
该办法回来此使命最近实践履行的组织履行时刻。在这段代码中,首要对 lock 目标进行同步,以确保线程安全。然后,依据 period 的值核算并回来下一次履行时刻。
- 假如 period 小于 0,则回来 nextExecutionTime + period;
- 不然,回来 nextExecutionTime – period。
撤销
public boolean cancel() {
synchronized(lock) {
boolean result = (state == SCHEDULED);
state = CANCELLED;
return result;
}
}
这段代码是 TimerTask 类中的 cancel 办法的完结。该办法用于撤销此计时器使命。假如使命已组织但没有履行,或许已组织重复履行,则撤销后不再运转。假如使命现已履行或许因其他原因无法撤销,则此办法回来 false;不然回来 true。
在这段代码中,首要对 lock 目标进行同步,以确保线程安全。然后,查看当时状况是否为 SCHEDULED,并将其保存在 result 变量中。接下来,将 state 设置为 CANCELLED,表明使命已被撤销。最终,回来 result 变量的值。
cancel 办法或许会被重复调用;第二次和后续的调用将会没有任何作用。
TaskQueue
TaskQueue 类表明一个计时器使命行列,它是一个依照 nextExecutionTime 排序的 TimerTask 优先行列。每个 Timer 目标都有一个这样的行列,并与其 TimerThread 同享。
在内部,TaskQueue 类运用堆来完结,这为 add、removeMin 和 rescheduleMin 操作供给了 log(n) 的功能,并为 getMin 操作供给了常数时刻功能。
数据结构
TaskQueue 类运用一个平衡二叉堆来表明优先行列,经过数组完结。在这个堆中,queue[n] 的两个子节点分别是 queue[2n] 和 queue[2n+1]。
优先行列依照 nextExecutionTime 字段排序:具有最小 nextExecutionTime 的 TimerTask 坐落 queue[1](假定行列非空)。关于堆中的每个节点 n,以及 n 的每个子孙 d,都有 n.nextExecutionTime <= d.nextExecutionTime。
private TimerTask[] queue = new TimerTask[128];
增加操作
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);
}
该办法用于向 TaskQueue 中增加新使命。
在这段代码中,首要查看当时行列的大小是否已满。假如 size + 1 等于 queue.length,则表明行列已满,需求扩容。此时,运用 Arrays.copyOf 办法将 queue 数组的长度扩大一倍。
接下来,将新使命增加到行列中,并调用 fixUp 办法对行列进行调整,以确保行列中的使命依照下一次履行时刻排序。
堆排序算法
fixUp
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;
}
}
这个办法是在将插入元素依据 nextExecutionTime 进行排序,每次右移一位,相当于除以 2 ,假如 k 方位的使命下一次履行时刻大于 k / 2 使命的履行时刻,就将数组在两者方位进行互换,然后以新方位 k / 2 作为输入进行下一次比较,
该办法用于在向 TaskQueue 中增加新使命时,对行列进行调整,以确保行列中的使命依照下一次履行时刻排序。
在这段代码中,首要界说一个 while 循环,循环条件为 k > 1
。然后,在循环内部,核算 j = k >> 1
(即 j 等于 k 除以 2)。接下来,查看 queue[j]
的 nextExecutionTime
是否小于等于 queue[k]
的 nextExecutionTime
。假如是,则退出循环;不然,交换 queue[j]
和 queue[k]
的方位,并将 k 设置为 j。最终,鄙人一次迭代中继续履行上述过程。
这个办法经过不断比较和交换元素的方位,来确保行列中的使命依照下一次履行时刻排序。
堆排序时刻复杂度为 log(n) ,所以 add 办法的时刻复杂度也是 log(n) 。
fixDown
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;
}
}
类似逻辑的办法是 fixDown ,该办法用于在从 TaskQueue 中删去使命时,对行列进行调整,以确保行列中的使命依照下一次履行时刻排序。
在这段代码中,首要界说一个 while 循环,循环条件为 j = k << 1
(即 j 等于 k 乘以 2)小于等于 size 而且 j 大于 0。然后,在循环内部,查看 j 是否小于 size,而且 queue[j]
的 nextExecutionTime
是否大于 queue[j+1
的 nextExecutionTime
。假如是,则将 j 加 1,使其指向最小的子节点。
接下来,查看 queue[k]
的 nextExecutionTime
是否小于等于 queue[j]
的 nextExecutionTime
。假如是,则退出循环;不然,交换 queue[j]
和 queue[k]
的方位,并将 k 设置为 j。最终,鄙人一次迭代中继续履行上述过程。
这个办法经过不断比较和交换元素的方位,来确保行列中的使命依照下一次履行时刻排序。
heapify
void heapify() {
for (int i = size/2; i >= 1; i--)
fixDown(i);
}
该办法用于对整个行列进行调整,以确保行列中的使命依照下一次履行时刻排序。
在这段代码中,首要界说一个 for 循环,循环变量 i 从 size/2 开端,每次递减 1,直到 i >= 1。然后,在循环内部,调用 fixDown 办法对第 i 个元素进行调整。
这个办法经过从后往前遍历行列中的元素,并对每个元素进行调整,来确保行列中的使命依照下一次履行时刻排序。
读取操作
访问元素系列办法很简单,直接查找数组索引,所以时刻复杂度是常量级别:
TimerTask getMin() {
return queue[1];
}
TimerTask get(int i) {
return queue[i];
}
删去操作
清理行列的办法是对数组挨个进行置空操作,size 更新为 0:
void clear() {
// Null out task references to prevent memory leak
for (int i=1; i<=size; i++)
queue[i] = null;
size = 0;
}
移除行列头一个元素,先经过将最终一个元素放到行列第一个方位上,这样达到了清理第一个元素的作用,然后经过 fixDown 办法对第一个元素进行排序,更新到适宜的方位上:
void removeMin() {
queue[1] = queue[size];
queue[size--] = null; // Drop extra reference to prevent memory leak
fixDown(1);
}
另一个办法是 quickRemove, 移除指定方位的元素,并没有进行排序:
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);
}
TimerThread
Timer 类中包含一个名为 TimerThread 的内部类,它完结了计时器的使命履行线程。这个线程负责等候计时器行列上的使命,当使命触发时履行它们,重新组织重复使命,并从行列中删去已撤销的使命和已完结的非重复使命。
先来看看它的特点:
boolean newTasksMayBeScheduled = true;
private TaskQueue queue;
TimerThread(TaskQueue queue) {
this.queue = queue;
}
这儿存储的引证不是 Timer 而是 TaskQueue,以使引证图坚持无环。不然,Timer 将永久不会被废物收回,这个线程也永久不会消失。
履行 run 办法来运转使命:
public void run() {
try {
mainLoop();
} finally {
// Someone killed this Thread, behave as if Timer cancelled
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // 删去过时的引证
}
}
}
中心逻辑在 mainLoop 办法中:
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) { // 非重复使命,删去
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule 重复使命,重新履行
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
if (!taskFired) // 使命没有发动; 等候
queue.wait(executionTime - currentTime);
}
if (taskFired) // 使命已发动; run it, holding no locks
task.run();
} catch(InterruptedException e) {}
}
}
该办法包含一个 while 循环,该循环会一向运转直到行列为空且不再组织新使命。
在循环中,它首要等候行列变为非空,然后查看行列中的第一个使命并履行相应操作。假如使命被撤销,则将其从行列中移除并继续轮询行列。假如使命现已抵达履行时刻,则依据使命是否重复履行相应操作:
- 关于非重复使命,将其从行列中移除并将其状况设置为已履行;
- 关于重复使命,重新组织履行时刻。假如使命没有发动,则等候直抵达到履行时刻。
最终,假如使命已发动,则在不持有任何锁的情况下运转它。
Timer
内部创立使命行列、线程:
@ReachabilitySensitive
private final TaskQueue queue = new TaskQueue();
@ReachabilitySensitive
private final TimerThread thread = new TimerThread(queue);
// 用于生成线程名
private final static AtomicInteger nextSerialNumber = new AtomicInteger(0);
private static int serialNumber() {
return nextSerialNumber.getAndIncrement();
}
废物收回时完毕线程
Timer 类中的 threadReaper 特点首要是为了当 Timer 类没有引证时,而且没有 task 在行列时,这时让线程 thread 高雅地完毕。这儿首要是重写了 Object 的 finalize 办法,在 GC 的时候进行调用,然后让线程完毕。
private final Object threadReaper = new Object() {
protected void finalize() throws Throwable {
synchronized(queue) {
thread.newTasksMayBeScheduled = false;
queue.notify(); // In case queue is empty.
}
}
};
结构办法
public Timer() {
this("Timer-" + serialNumber());
}
// isDaemon 表明相关的线程是否应该作为看护进程运转。
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();
}
结构办法中存在 isDaemon 参数,当线程设置为看护线程时,Java虚拟机在当仅有运转的线程都是看护线程时才会退出。
这儿的参数都是设置给 TimerThread 的,而且调用了 start 办法开端发动线程履行使命。
schedule 系列办法
上面这组办法对时刻间隔要求比较高,适用于需求“流畅性”的重复活动。换句话说,它适用于在短期内坚持频率精确比在长期内坚持频率精确更重要的活动。这包含大多数动画使命,如守时闪耀光标。它还包含在响应人工输入时履行常规活动的使命,例如只要按下键就自动重复一个字符。
在固定推迟履行中,**每次履行都是相关于前一次履行的实践履行时刻进行组织的。假如某次履行因任何原因(如废物收回或其他后台活动)而被推迟,则后续履行也将被推迟。**从长远来看,履行频率通常会略低于指定周期的倒数(假定底层Object.wait(long)的体系时钟精确)。
// 发布必定推迟时刻后履行的使命
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);
}
// 发布必定推迟时刻后,随后以周期 period 履行使命,每次履行都是相关于前一次履行的实践履行时刻来组织的。
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);
}
// 发布指定日期后首次履行使命,随后以周期 period 履行使命。
public void schedule(TimerTask task, Date firstTime, long period) {
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, firstTime.getTime(), -period);
}
在固定速率履行中,**每次履行都是相关于初始履行的预订履行时刻进行组织的。假如某次履行因任何原因(如废物收回或其他后台活动)而被推迟,则两次或多次履行将快速接连进行以“追赶”。**从长远来看,履行频率将刚好为指定周期的倒数(假定底层 Object.wait(long) 的体系时钟精确)。
固定速率履行适用于对肯定时刻灵敏的重复活动,例如每小时整点敲响钟声,或许每天在特守时刻运转方案维护。它也适用于需求在固守时刻内完结固定数量次数的重复活动,例如倒计时器每秒钟滴答一次,继续十秒钟。最终,固定速率履行适用于组织多个必须彼此同步的重复计时器使命。
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);
}
sched
这个办法是一切 schedule 系列办法的中心完结:
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();
}
}
它承受一个TimerTask目标,一个履行时刻和一个周期作为参数。首要,它查看履行时刻是否小于0:假如是,则抛出 IllegalArgumentException 。然后,它对周期值进行束缚,以防止数值溢出,同时仍然有效地无限大。
接下来,在同步行列的情况下,它查看线程是否允许组织新使命。假如不允许,则抛出 IllegalStateException 。
然后,在同步使命锁的情况下,它查看使命状况是否为VIRGIN(未组织)。
假如不是,则抛出 IllegalStateException 。然后,它设置使命的下一次履行时刻、周期和状况。
最终,在同步行列的情况下,将使命增加到行列中,并查看行列中的最小元素是否为该使命。假如是,则通知行列。
撤销操作
cancel
public void cancel() {
synchronized(queue) {
thread.newTasksMayBeScheduled = false;
queue.clear();
queue.notify(); // In case queue was already empty.
}
}
停止此计时器,丢弃任何当时方案的使命。不会干扰当时正在履行的使命(假如存在)。一旦计时器被停止,它的履行线程就会高雅地停止,而且不再能够在其上调度任何使命。
请注意,从由该计时器调用的计时器使命的 run 办法中调用此办法肯定确保正在履行的使命是该计时器将履行的最终一个使命履行。
这个办法能够被重复调用;第二次和随后的调用没有作用。
从该计时器的使命行列中移除一切已撤销的使命。调用此办法对计时器的行为没有影响,但会从行列中消除对已撤销使命的引证。假如没有对这些使命的外部引证,它们就有资历进行废物搜集。
purge
大多数程序都不需求调用这个办法。它是为撤销很多使命的稀有应用程序而规划的。调用此办法以时刻交换空间:该办法的运转时刻或许与 O(n) + O(c * log n) 成正比,其间 n 是行列中的使命数量,c 是撤销的使命数量。允许在此计时器上调度的使命中调用此办法。回来值是从行列中移除的使命数。
public int purge() {
int result = 0;
synchronized(queue) {
for (int i = queue.size(); i > 0; i--) {
if (queue.get(i).state == TimerTask.CANCELLED) {
queue.quickRemove(i);
result++;
}
}
if (result != 0)
queue.heapify();
}
return result;
}
从行列中移除一切已撤销的使命,并回来已移除使命的数量。首要,它在同步行列的情况下,遍历行列中的一切元素。关于每个元素,假如其状况为CANCELLED,则运用quickRemove办法将其从行列中移除,并将成果计数器递加。
遍历完结后,假如成果计数器不为0,则调用heapify办法重新构建堆。最终,回来成果计数器的值。